search1api 0.2.1__tar.gz → 0.3.0__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.
- {search1api-0.2.1 → search1api-0.3.0}/PKG-INFO +40 -2
- {search1api-0.2.1 → search1api-0.3.0}/README.md +39 -1
- {search1api-0.2.1 → search1api-0.3.0}/openapi/search1api.openapi.json +502 -9
- {search1api-0.2.1 → search1api-0.3.0}/pyproject.toml +1 -1
- {search1api-0.2.1 → search1api-0.3.0}/src/search1api/__init__.py +1 -1
- {search1api-0.2.1 → search1api-0.3.0}/src/search1api/client.py +117 -5
- {search1api-0.2.1 → search1api-0.3.0}/src/search1api/types.py +54 -1
- {search1api-0.2.1 → search1api-0.3.0}/tests/test_client.py +119 -0
- {search1api-0.2.1 → search1api-0.3.0}/.github/workflows/ci.yml +0 -0
- {search1api-0.2.1 → search1api-0.3.0}/.github/workflows/release.yml +0 -0
- {search1api-0.2.1 → search1api-0.3.0}/.gitignore +0 -0
- {search1api-0.2.1 → search1api-0.3.0}/LICENSE +0 -0
- {search1api-0.2.1 → search1api-0.3.0}/openapi/README.md +0 -0
- {search1api-0.2.1 → search1api-0.3.0}/src/search1api/errors.py +0 -0
- {search1api-0.2.1 → search1api-0.3.0}/src/search1api/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: search1api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Official Python client for Search1API
|
|
5
5
|
Project-URL: Homepage, https://s1.dev
|
|
6
6
|
Project-URL: Documentation, https://s1.dev/docs/integrations/sdks
|
|
@@ -77,6 +77,12 @@ for result in response["results"]:
|
|
|
77
77
|
print(result["title"], result["link"])
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
`search_service` selects one engine (`google` by default), for example `bing`,
|
|
81
|
+
`bingcn`, `yandex`, `reddit`, `github`, `arxiv`, `wikipedia`, or `grokipedia`.
|
|
82
|
+
`page` requests a later results page on engines with native pagination
|
|
83
|
+
(`bing`, `bingcn`, `baidu`, `grokipedia`). Results carry `published_date` when the source
|
|
84
|
+
exposes one.
|
|
85
|
+
|
|
80
86
|
Use the client as a context manager when it owns the HTTP connection pool:
|
|
81
87
|
|
|
82
88
|
```python
|
|
@@ -93,6 +99,24 @@ async with AsyncSearch1API() as client:
|
|
|
93
99
|
response = await client.search("latest AI agent frameworks")
|
|
94
100
|
```
|
|
95
101
|
|
|
102
|
+
## Ask
|
|
103
|
+
|
|
104
|
+
`ask` sends a natural-language request and lets Search1API choose the engines
|
|
105
|
+
and time window. It returns at most 10 results ranked by relevance, and
|
|
106
|
+
`intent` reports what was searched:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
answer = client.ask("What are developers saying about Bun 1.3 this month?")
|
|
110
|
+
|
|
111
|
+
print(answer["intent"]["sources"], answer["intent"]["time_range"])
|
|
112
|
+
for result in answer["results"]:
|
|
113
|
+
print(result["relevance"], result["source"], result["title"], result["link"])
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A completed request costs 5 credits. Ask is not available with pay-per-request
|
|
117
|
+
payments, and its default timeout is 45 seconds. Use `search` when you already
|
|
118
|
+
know which engine and keywords you want.
|
|
119
|
+
|
|
96
120
|
## Deepcrawl
|
|
97
121
|
|
|
98
122
|
`deepcrawl` starts a task and waits for it to finish:
|
|
@@ -126,7 +150,21 @@ The clients also support news, crawl, sitemap, trending, extract, usage, and
|
|
|
126
150
|
batch operations exposed by the Search1API HTTP API. Requests time out after
|
|
127
151
|
30 seconds and retry `429` and transient `5xx` responses twice by default.
|
|
128
152
|
Authentication, payment, and validation errors are never retried. Deepcrawl
|
|
129
|
-
task creation
|
|
153
|
+
task creation and feedback are not retried automatically because they are not
|
|
154
|
+
idempotent.
|
|
155
|
+
|
|
156
|
+
## Feedback
|
|
157
|
+
|
|
158
|
+
`feedback` reports a Search1API problem, missing capability, or confusing
|
|
159
|
+
documentation. It is free. Do not include credentials or personal data:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
client.feedback(
|
|
163
|
+
"Results for this query have no publication dates",
|
|
164
|
+
category="feature_request",
|
|
165
|
+
request_id="the x-search1api-request-id of the original request",
|
|
166
|
+
)
|
|
167
|
+
```
|
|
130
168
|
|
|
131
169
|
## Development
|
|
132
170
|
|
|
@@ -26,6 +26,12 @@ for result in response["results"]:
|
|
|
26
26
|
print(result["title"], result["link"])
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
`search_service` selects one engine (`google` by default), for example `bing`,
|
|
30
|
+
`bingcn`, `yandex`, `reddit`, `github`, `arxiv`, `wikipedia`, or `grokipedia`.
|
|
31
|
+
`page` requests a later results page on engines with native pagination
|
|
32
|
+
(`bing`, `bingcn`, `baidu`, `grokipedia`). Results carry `published_date` when the source
|
|
33
|
+
exposes one.
|
|
34
|
+
|
|
29
35
|
Use the client as a context manager when it owns the HTTP connection pool:
|
|
30
36
|
|
|
31
37
|
```python
|
|
@@ -42,6 +48,24 @@ async with AsyncSearch1API() as client:
|
|
|
42
48
|
response = await client.search("latest AI agent frameworks")
|
|
43
49
|
```
|
|
44
50
|
|
|
51
|
+
## Ask
|
|
52
|
+
|
|
53
|
+
`ask` sends a natural-language request and lets Search1API choose the engines
|
|
54
|
+
and time window. It returns at most 10 results ranked by relevance, and
|
|
55
|
+
`intent` reports what was searched:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
answer = client.ask("What are developers saying about Bun 1.3 this month?")
|
|
59
|
+
|
|
60
|
+
print(answer["intent"]["sources"], answer["intent"]["time_range"])
|
|
61
|
+
for result in answer["results"]:
|
|
62
|
+
print(result["relevance"], result["source"], result["title"], result["link"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A completed request costs 5 credits. Ask is not available with pay-per-request
|
|
66
|
+
payments, and its default timeout is 45 seconds. Use `search` when you already
|
|
67
|
+
know which engine and keywords you want.
|
|
68
|
+
|
|
45
69
|
## Deepcrawl
|
|
46
70
|
|
|
47
71
|
`deepcrawl` starts a task and waits for it to finish:
|
|
@@ -75,7 +99,21 @@ The clients also support news, crawl, sitemap, trending, extract, usage, and
|
|
|
75
99
|
batch operations exposed by the Search1API HTTP API. Requests time out after
|
|
76
100
|
30 seconds and retry `429` and transient `5xx` responses twice by default.
|
|
77
101
|
Authentication, payment, and validation errors are never retried. Deepcrawl
|
|
78
|
-
task creation
|
|
102
|
+
task creation and feedback are not retried automatically because they are not
|
|
103
|
+
idempotent.
|
|
104
|
+
|
|
105
|
+
## Feedback
|
|
106
|
+
|
|
107
|
+
`feedback` reports a Search1API problem, missing capability, or confusing
|
|
108
|
+
documentation. It is free. Do not include credentials or personal data:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
client.feedback(
|
|
112
|
+
"Results for this query have no publication dates",
|
|
113
|
+
category="feature_request",
|
|
114
|
+
request_id="the x-search1api-request-id of the original request",
|
|
115
|
+
)
|
|
116
|
+
```
|
|
79
117
|
|
|
80
118
|
## Development
|
|
81
119
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"title": "Search1API",
|
|
5
5
|
"description": "Web search, news, crawling, screenshot, and content extraction APIs for AI agents. Supports pay-per-request via MPP and x402 protocols.",
|
|
6
6
|
"version": "1.0.0",
|
|
7
|
-
"x-guidance": "Search1API provides web search, news aggregation, URL crawling, webpage screenshots, sitemap extraction, trending topics, content extraction, and deep crawling. All paid endpoints accept POST with a JSON body. Use POST /search with a \"query\" field for web search. Use POST /news with a \"query\" field for news. Use POST /crawl with a \"url\" field to crawl a page. Use POST /screenshot with a \"url\" field to render a PNG, JPEG, or WebP image. Use POST /sitemap with a \"url\" field to extract sitemap URLs. Use POST /trending with a \"search_service\" field for trends. Use POST /extract with a \"url\" field for structured content extraction. Use POST /deepcrawl with a \"url\" field for deep multi-page crawling."
|
|
7
|
+
"x-guidance": "Search1API provides web search, news aggregation, URL crawling, webpage screenshots, sitemap extraction, trending topics, content extraction, and deep crawling. All paid endpoints accept POST with a JSON body. Use POST /search with a \"query\" field for web search. Use POST /news with a \"query\" field for news. Use POST /ask with a natural-language \"query\" to have Search1API choose the engines and time window and return only relevant results (API key only). Use POST /crawl with a \"url\" field to crawl a page. Use POST /screenshot with a \"url\" field to render a PNG, JPEG, or WebP image. Use POST /sitemap with a \"url\" field to extract sitemap URLs. Use POST /trending with a \"search_service\" field for trends. Use POST /extract with a \"url\" field for structured content extraction. Use POST /deepcrawl with a \"url\" field for deep multi-page crawling."
|
|
8
8
|
},
|
|
9
9
|
"x-discovery": {
|
|
10
10
|
"ownershipProofs": []
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"schemas": {
|
|
26
26
|
"ApiError": {
|
|
27
27
|
"type": "object",
|
|
28
|
-
"description": "Search1API error. Payment challenges may use RFC 9457 problem detail fields.",
|
|
28
|
+
"description": "Search1API error. Every JSON error carries `ok: false`, `error` (a short status-derived label) and `message` (human-readable detail); validation failures add `errors`. Payment challenges may use RFC 9457 problem detail fields.",
|
|
29
29
|
"properties": {
|
|
30
30
|
"ok": {
|
|
31
31
|
"type": "boolean",
|
|
@@ -84,8 +84,10 @@
|
|
|
84
84
|
"enum": [
|
|
85
85
|
"google",
|
|
86
86
|
"bing",
|
|
87
|
+
"bingcn",
|
|
87
88
|
"duckduckgo",
|
|
88
89
|
"yahoo",
|
|
90
|
+
"yandex",
|
|
89
91
|
"youtube",
|
|
90
92
|
"x",
|
|
91
93
|
"reddit",
|
|
@@ -95,8 +97,8 @@
|
|
|
95
97
|
"bilibili",
|
|
96
98
|
"imdb",
|
|
97
99
|
"wikipedia",
|
|
100
|
+
"grokipedia",
|
|
98
101
|
"",
|
|
99
|
-
"sogou",
|
|
100
102
|
"baidu",
|
|
101
103
|
"360",
|
|
102
104
|
"quark"
|
|
@@ -108,6 +110,12 @@
|
|
|
108
110
|
"maximum": 50,
|
|
109
111
|
"default": 5
|
|
110
112
|
},
|
|
113
|
+
"page": {
|
|
114
|
+
"type": "integer",
|
|
115
|
+
"minimum": 1,
|
|
116
|
+
"maximum": 100,
|
|
117
|
+
"default": 1
|
|
118
|
+
},
|
|
111
119
|
"crawl_results": {
|
|
112
120
|
"type": "integer",
|
|
113
121
|
"minimum": 0,
|
|
@@ -334,6 +342,42 @@
|
|
|
334
342
|
},
|
|
335
343
|
"content": {
|
|
336
344
|
"type": "string"
|
|
345
|
+
},
|
|
346
|
+
"published_date": {
|
|
347
|
+
"type": "string",
|
|
348
|
+
"description": "When the page was published, as ISO 8601: `YYYY-MM-DD` when only the day is known (web search engines), or `YYYY-MM-DDTHH:MM:SSZ` in UTC when the source carries a time (x, reddit, github, arxiv, youtube, bilibili, wechat, news feeds). Omitted when the source exposes no date, so treat it as optional per result.",
|
|
349
|
+
"example": "2026-09-03"
|
|
350
|
+
},
|
|
351
|
+
"kind": {
|
|
352
|
+
"type": "string",
|
|
353
|
+
"enum": [
|
|
354
|
+
"repo",
|
|
355
|
+
"issue",
|
|
356
|
+
"pr",
|
|
357
|
+
"discussion"
|
|
358
|
+
],
|
|
359
|
+
"description": "What a `github` result is: a repository, an issue, a pull request, or a discussion. Only present for `search_service: \"github\"`."
|
|
360
|
+
},
|
|
361
|
+
"stars": {
|
|
362
|
+
"type": "integer",
|
|
363
|
+
"description": "Stargazer count of a `github` repository result (`kind: \"repo\"`)."
|
|
364
|
+
},
|
|
365
|
+
"language": {
|
|
366
|
+
"type": "string",
|
|
367
|
+
"description": "Primary language of a `github` repository result (`kind: \"repo\"`). Omitted when GitHub reports none."
|
|
368
|
+
},
|
|
369
|
+
"num_comments": {
|
|
370
|
+
"type": "integer",
|
|
371
|
+
"description": "Comment count on a `github` issue, pull request or discussion, or on a `hackernews` thread (POST /news)."
|
|
372
|
+
},
|
|
373
|
+
"story_url": {
|
|
374
|
+
"type": "string",
|
|
375
|
+
"format": "uri",
|
|
376
|
+
"description": "POST /news with `search_service: \"hackernews\"` only: the submitted article. `link` is the Hacker News discussion thread; `story_url` is empty for Ask HN / Show HN text posts."
|
|
377
|
+
},
|
|
378
|
+
"points": {
|
|
379
|
+
"type": "integer",
|
|
380
|
+
"description": "POST /news with `search_service: \"hackernews\"` only: upvotes on the thread."
|
|
337
381
|
}
|
|
338
382
|
},
|
|
339
383
|
"additionalProperties": true
|
|
@@ -526,6 +570,120 @@
|
|
|
526
570
|
}
|
|
527
571
|
}
|
|
528
572
|
},
|
|
573
|
+
"AskResult": {
|
|
574
|
+
"type": "object",
|
|
575
|
+
"required": [
|
|
576
|
+
"title",
|
|
577
|
+
"link",
|
|
578
|
+
"snippet",
|
|
579
|
+
"source",
|
|
580
|
+
"relevance"
|
|
581
|
+
],
|
|
582
|
+
"properties": {
|
|
583
|
+
"title": {
|
|
584
|
+
"type": "string"
|
|
585
|
+
},
|
|
586
|
+
"link": {
|
|
587
|
+
"type": "string",
|
|
588
|
+
"format": "uri"
|
|
589
|
+
},
|
|
590
|
+
"snippet": {
|
|
591
|
+
"type": "string"
|
|
592
|
+
},
|
|
593
|
+
"published_date": {
|
|
594
|
+
"type": "string",
|
|
595
|
+
"description": "When the page was published, as ISO 8601 (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ`). Omitted when the engine exposes no date.",
|
|
596
|
+
"example": "2026-09-03"
|
|
597
|
+
},
|
|
598
|
+
"source": {
|
|
599
|
+
"type": "string",
|
|
600
|
+
"description": "Engine that returned the result."
|
|
601
|
+
},
|
|
602
|
+
"relevance": {
|
|
603
|
+
"type": "number",
|
|
604
|
+
"minimum": 0,
|
|
605
|
+
"maximum": 1,
|
|
606
|
+
"description": "How likely the result is to be about the query, two decimals. Only results scoring at least 0.5 are returned."
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
"AskResponse": {
|
|
611
|
+
"type": "object",
|
|
612
|
+
"required": [
|
|
613
|
+
"query",
|
|
614
|
+
"intent",
|
|
615
|
+
"results",
|
|
616
|
+
"errors"
|
|
617
|
+
],
|
|
618
|
+
"properties": {
|
|
619
|
+
"query": {
|
|
620
|
+
"type": "string",
|
|
621
|
+
"description": "The query as sent."
|
|
622
|
+
},
|
|
623
|
+
"intent": {
|
|
624
|
+
"type": "object",
|
|
625
|
+
"required": [
|
|
626
|
+
"search_query",
|
|
627
|
+
"sources",
|
|
628
|
+
"time_range"
|
|
629
|
+
],
|
|
630
|
+
"description": "How the request was interpreted and searched.",
|
|
631
|
+
"properties": {
|
|
632
|
+
"search_query": {
|
|
633
|
+
"type": "string",
|
|
634
|
+
"description": "Keywords sent to the engines, with platform and time phrases removed."
|
|
635
|
+
},
|
|
636
|
+
"sources": {
|
|
637
|
+
"type": "array",
|
|
638
|
+
"items": {
|
|
639
|
+
"type": "string"
|
|
640
|
+
},
|
|
641
|
+
"description": "Engines searched."
|
|
642
|
+
},
|
|
643
|
+
"time_range": {
|
|
644
|
+
"type": [
|
|
645
|
+
"string",
|
|
646
|
+
"null"
|
|
647
|
+
],
|
|
648
|
+
"enum": [
|
|
649
|
+
"day",
|
|
650
|
+
"week",
|
|
651
|
+
"month",
|
|
652
|
+
"year",
|
|
653
|
+
null
|
|
654
|
+
],
|
|
655
|
+
"description": "Publication window applied, or null for none."
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
"results": {
|
|
660
|
+
"type": "array",
|
|
661
|
+
"items": {
|
|
662
|
+
"$ref": "#/components/schemas/AskResult"
|
|
663
|
+
},
|
|
664
|
+
"description": "Relevant results from every engine, best first, with duplicates merged. May be empty."
|
|
665
|
+
},
|
|
666
|
+
"errors": {
|
|
667
|
+
"type": "array",
|
|
668
|
+
"description": "Engines that failed while others completed. The request is still charged.",
|
|
669
|
+
"items": {
|
|
670
|
+
"type": "object",
|
|
671
|
+
"required": [
|
|
672
|
+
"source",
|
|
673
|
+
"message"
|
|
674
|
+
],
|
|
675
|
+
"properties": {
|
|
676
|
+
"source": {
|
|
677
|
+
"type": "string"
|
|
678
|
+
},
|
|
679
|
+
"message": {
|
|
680
|
+
"type": "string"
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
},
|
|
529
687
|
"CrawlResult": {
|
|
530
688
|
"type": "object",
|
|
531
689
|
"required": [
|
|
@@ -771,10 +929,191 @@
|
|
|
771
929
|
]
|
|
772
930
|
}
|
|
773
931
|
}
|
|
932
|
+
},
|
|
933
|
+
"FeedbackResponse": {
|
|
934
|
+
"type": "object",
|
|
935
|
+
"required": [
|
|
936
|
+
"id",
|
|
937
|
+
"status"
|
|
938
|
+
],
|
|
939
|
+
"properties": {
|
|
940
|
+
"id": {
|
|
941
|
+
"type": "string",
|
|
942
|
+
"pattern": "^fb_[a-zA-Z0-9_-]+$",
|
|
943
|
+
"maxLength": 80
|
|
944
|
+
},
|
|
945
|
+
"status": {
|
|
946
|
+
"type": "string",
|
|
947
|
+
"enum": [
|
|
948
|
+
"new"
|
|
949
|
+
]
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
"additionalProperties": false
|
|
774
953
|
}
|
|
775
954
|
}
|
|
776
955
|
},
|
|
777
956
|
"paths": {
|
|
957
|
+
"/feedback": {
|
|
958
|
+
"post": {
|
|
959
|
+
"operationId": "feedback",
|
|
960
|
+
"summary": "Report a Search1API problem or feature request",
|
|
961
|
+
"description": "Report a Search1API problem, missing capability, or confusing documentation encountered during a task. Only message is required. Optional request_id refers to the original problem request. Do not include credentials, personal data, or private user content. Reports are stored in Agentback and linked to an opaque account reference. Do not report every empty result or retry the same report automatically; feedback should not block the main task. Returns 201 only after storage succeeds. Maximum JSON body size: 16 KiB. Limited to 10 submissions per account per minute at a Cloudflare location. Free to call.",
|
|
962
|
+
"tags": [
|
|
963
|
+
"Feedback"
|
|
964
|
+
],
|
|
965
|
+
"responses": {
|
|
966
|
+
"201": {
|
|
967
|
+
"description": "Successful response",
|
|
968
|
+
"content": {
|
|
969
|
+
"application/json": {
|
|
970
|
+
"schema": {
|
|
971
|
+
"$ref": "#/components/schemas/FeedbackResponse"
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
"400": {
|
|
977
|
+
"description": "Bad Request",
|
|
978
|
+
"content": {
|
|
979
|
+
"application/json": {
|
|
980
|
+
"schema": {
|
|
981
|
+
"$ref": "#/components/schemas/ApiError"
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
"401": {
|
|
987
|
+
"description": "Unauthorized",
|
|
988
|
+
"content": {
|
|
989
|
+
"application/json": {
|
|
990
|
+
"schema": {
|
|
991
|
+
"$ref": "#/components/schemas/ApiError"
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
"403": {
|
|
997
|
+
"description": "Forbidden",
|
|
998
|
+
"content": {
|
|
999
|
+
"application/json": {
|
|
1000
|
+
"schema": {
|
|
1001
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
"413": {
|
|
1007
|
+
"description": "Payload Too Large",
|
|
1008
|
+
"content": {
|
|
1009
|
+
"application/json": {
|
|
1010
|
+
"schema": {
|
|
1011
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
"415": {
|
|
1017
|
+
"description": "Unsupported Media Type",
|
|
1018
|
+
"content": {
|
|
1019
|
+
"application/json": {
|
|
1020
|
+
"schema": {
|
|
1021
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
"429": {
|
|
1027
|
+
"description": "Too Many Requests",
|
|
1028
|
+
"content": {
|
|
1029
|
+
"application/json": {
|
|
1030
|
+
"schema": {
|
|
1031
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
"500": {
|
|
1037
|
+
"description": "Internal Server Error",
|
|
1038
|
+
"content": {
|
|
1039
|
+
"application/json": {
|
|
1040
|
+
"schema": {
|
|
1041
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
"503": {
|
|
1047
|
+
"description": "Service Unavailable",
|
|
1048
|
+
"content": {
|
|
1049
|
+
"application/json": {
|
|
1050
|
+
"schema": {
|
|
1051
|
+
"$ref": "#/components/schemas/ApiError"
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
"security": [
|
|
1058
|
+
{
|
|
1059
|
+
"bearerAuth": []
|
|
1060
|
+
}
|
|
1061
|
+
],
|
|
1062
|
+
"requestBody": {
|
|
1063
|
+
"required": true,
|
|
1064
|
+
"content": {
|
|
1065
|
+
"application/json": {
|
|
1066
|
+
"schema": {
|
|
1067
|
+
"type": "object",
|
|
1068
|
+
"properties": {
|
|
1069
|
+
"message": {
|
|
1070
|
+
"type": "string",
|
|
1071
|
+
"minLength": 1,
|
|
1072
|
+
"maxLength": 4000
|
|
1073
|
+
},
|
|
1074
|
+
"intent": {
|
|
1075
|
+
"type": "string",
|
|
1076
|
+
"maxLength": 2000
|
|
1077
|
+
},
|
|
1078
|
+
"category": {
|
|
1079
|
+
"type": "string",
|
|
1080
|
+
"enum": [
|
|
1081
|
+
"bug",
|
|
1082
|
+
"feature_request",
|
|
1083
|
+
"docs",
|
|
1084
|
+
"other"
|
|
1085
|
+
],
|
|
1086
|
+
"default": "other"
|
|
1087
|
+
},
|
|
1088
|
+
"request_id": {
|
|
1089
|
+
"type": "string",
|
|
1090
|
+
"maxLength": 256
|
|
1091
|
+
},
|
|
1092
|
+
"agent": {
|
|
1093
|
+
"type": "object",
|
|
1094
|
+
"properties": {
|
|
1095
|
+
"name": {
|
|
1096
|
+
"type": "string",
|
|
1097
|
+
"maxLength": 128
|
|
1098
|
+
},
|
|
1099
|
+
"model": {
|
|
1100
|
+
"type": "string",
|
|
1101
|
+
"maxLength": 128
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
"additionalProperties": false
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
"required": [
|
|
1108
|
+
"message"
|
|
1109
|
+
],
|
|
1110
|
+
"additionalProperties": false
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
778
1117
|
"/screenshot": {
|
|
779
1118
|
"post": {
|
|
780
1119
|
"operationId": "screenshot",
|
|
@@ -1099,7 +1438,7 @@
|
|
|
1099
1438
|
"post": {
|
|
1100
1439
|
"operationId": "search",
|
|
1101
1440
|
"summary": "Search the web using multiple search engines",
|
|
1102
|
-
"description": "Search the live public web when the answer depends on current information, sources, or research a model's training data cannot cover. Returns ranked results with id, title, URL, and
|
|
1441
|
+
"description": "Search the live public web when the answer depends on current information, sources, or research a model's training data cannot cover. Returns ranked results with id, title, URL, snippet, and `published_date` (ISO 8601, when the engine exposes one) across 13+ engines, with optional images. Set `crawl_results` to pull the top N result pages in the same call — each crawled page is billed as an additional crawl — or pass a result URL to POST /crawl separately. Costs 1 credit per request.",
|
|
1103
1442
|
"tags": [
|
|
1104
1443
|
"Search"
|
|
1105
1444
|
],
|
|
@@ -1236,8 +1575,10 @@
|
|
|
1236
1575
|
"enum": [
|
|
1237
1576
|
"google",
|
|
1238
1577
|
"bing",
|
|
1578
|
+
"bingcn",
|
|
1239
1579
|
"duckduckgo",
|
|
1240
1580
|
"yahoo",
|
|
1581
|
+
"yandex",
|
|
1241
1582
|
"youtube",
|
|
1242
1583
|
"x",
|
|
1243
1584
|
"reddit",
|
|
@@ -1247,8 +1588,8 @@
|
|
|
1247
1588
|
"bilibili",
|
|
1248
1589
|
"imdb",
|
|
1249
1590
|
"wikipedia",
|
|
1591
|
+
"grokipedia",
|
|
1250
1592
|
"",
|
|
1251
|
-
"sogou",
|
|
1252
1593
|
"baidu",
|
|
1253
1594
|
"360",
|
|
1254
1595
|
"quark"
|
|
@@ -1260,6 +1601,12 @@
|
|
|
1260
1601
|
"maximum": 50,
|
|
1261
1602
|
"default": 5
|
|
1262
1603
|
},
|
|
1604
|
+
"page": {
|
|
1605
|
+
"type": "integer",
|
|
1606
|
+
"minimum": 1,
|
|
1607
|
+
"maximum": 100,
|
|
1608
|
+
"default": 1
|
|
1609
|
+
},
|
|
1263
1610
|
"crawl_results": {
|
|
1264
1611
|
"type": "integer",
|
|
1265
1612
|
"minimum": 0,
|
|
@@ -1317,8 +1664,10 @@
|
|
|
1317
1664
|
"enum": [
|
|
1318
1665
|
"google",
|
|
1319
1666
|
"bing",
|
|
1667
|
+
"bingcn",
|
|
1320
1668
|
"duckduckgo",
|
|
1321
1669
|
"yahoo",
|
|
1670
|
+
"yandex",
|
|
1322
1671
|
"youtube",
|
|
1323
1672
|
"x",
|
|
1324
1673
|
"reddit",
|
|
@@ -1328,8 +1677,8 @@
|
|
|
1328
1677
|
"bilibili",
|
|
1329
1678
|
"imdb",
|
|
1330
1679
|
"wikipedia",
|
|
1680
|
+
"grokipedia",
|
|
1331
1681
|
"",
|
|
1332
|
-
"sogou",
|
|
1333
1682
|
"baidu",
|
|
1334
1683
|
"360",
|
|
1335
1684
|
"quark"
|
|
@@ -1341,6 +1690,12 @@
|
|
|
1341
1690
|
"maximum": 50,
|
|
1342
1691
|
"default": 5
|
|
1343
1692
|
},
|
|
1693
|
+
"page": {
|
|
1694
|
+
"type": "integer",
|
|
1695
|
+
"minimum": 1,
|
|
1696
|
+
"maximum": 100,
|
|
1697
|
+
"default": 1
|
|
1698
|
+
},
|
|
1344
1699
|
"crawl_results": {
|
|
1345
1700
|
"type": "integer",
|
|
1346
1701
|
"minimum": 0,
|
|
@@ -1397,7 +1752,7 @@
|
|
|
1397
1752
|
"post": {
|
|
1398
1753
|
"operationId": "news",
|
|
1399
1754
|
"summary": "Search news articles across multiple sources",
|
|
1400
|
-
"description": "Search recent news when the question is about events, announcements, or coverage rather than reference material. Returns articles from verified publishers with title, URL, snippet, and optional full page content, filterable by site, language, and time range. For questions that are not time-sensitive, prefer POST /search. Costs 1 credit per request.",
|
|
1755
|
+
"description": "Search recent news when the question is about events, announcements, or coverage rather than reference material. Returns articles from verified publishers with title, URL, snippet, `published_date` (ISO 8601, when known), and optional full page content, filterable by site, language, and time range. For questions that are not time-sensitive, prefer POST /search. Costs 1 credit per request.",
|
|
1401
1756
|
"tags": [
|
|
1402
1757
|
"Search"
|
|
1403
1758
|
],
|
|
@@ -1669,11 +2024,129 @@
|
|
|
1669
2024
|
}
|
|
1670
2025
|
}
|
|
1671
2026
|
},
|
|
2027
|
+
"/ask": {
|
|
2028
|
+
"post": {
|
|
2029
|
+
"operationId": "ask",
|
|
2030
|
+
"summary": "Search the engines a natural-language request calls for",
|
|
2031
|
+
"description": "Agentic search. Describe what you are looking for in plain language and let a decision model (currently TypeSafe Jev) decide where to look. It picks up to five engines (web search, Hacker News, Reddit, GitHub, X, arXiv, Wikipedia, IMDb, WeChat, YouTube), rewrites the request into search keywords, infers a publication window from phrases such as \"this week\", searches the engines in parallel, and returns only the results judged relevant, merged and ranked, so agents read fewer off-topic results. The request takes only `query`; engines, keywords, and the window are always chosen by the model, and the response reports them in `intent`. Use POST /search instead when you already know the engine and keywords and want its raw ranking. Costs 5 credits per request.",
|
|
2032
|
+
"tags": [
|
|
2033
|
+
"Search"
|
|
2034
|
+
],
|
|
2035
|
+
"responses": {
|
|
2036
|
+
"200": {
|
|
2037
|
+
"description": "Successful response",
|
|
2038
|
+
"content": {
|
|
2039
|
+
"application/json": {
|
|
2040
|
+
"schema": {
|
|
2041
|
+
"$ref": "#/components/schemas/AskResponse"
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2046
|
+
"400": {
|
|
2047
|
+
"description": "Bad Request",
|
|
2048
|
+
"content": {
|
|
2049
|
+
"application/json": {
|
|
2050
|
+
"schema": {
|
|
2051
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
},
|
|
2056
|
+
"401": {
|
|
2057
|
+
"description": "Unauthorized",
|
|
2058
|
+
"content": {
|
|
2059
|
+
"application/json": {
|
|
2060
|
+
"schema": {
|
|
2061
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
"402": {
|
|
2067
|
+
"description": "Payment Required",
|
|
2068
|
+
"content": {
|
|
2069
|
+
"application/problem+json": {
|
|
2070
|
+
"schema": {
|
|
2071
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
},
|
|
2076
|
+
"429": {
|
|
2077
|
+
"description": "Too Many Requests",
|
|
2078
|
+
"content": {
|
|
2079
|
+
"application/json": {
|
|
2080
|
+
"schema": {
|
|
2081
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
"500": {
|
|
2087
|
+
"description": "Internal Server Error",
|
|
2088
|
+
"content": {
|
|
2089
|
+
"application/json": {
|
|
2090
|
+
"schema": {
|
|
2091
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
"502": {
|
|
2097
|
+
"description": "Bad Gateway",
|
|
2098
|
+
"content": {
|
|
2099
|
+
"application/json": {
|
|
2100
|
+
"schema": {
|
|
2101
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
"security": [
|
|
2108
|
+
{
|
|
2109
|
+
"bearerAuth": []
|
|
2110
|
+
}
|
|
2111
|
+
],
|
|
2112
|
+
"requestBody": {
|
|
2113
|
+
"required": true,
|
|
2114
|
+
"content": {
|
|
2115
|
+
"application/json": {
|
|
2116
|
+
"schema": {
|
|
2117
|
+
"type": "object",
|
|
2118
|
+
"properties": {
|
|
2119
|
+
"query": {
|
|
2120
|
+
"type": "string",
|
|
2121
|
+
"minLength": 1,
|
|
2122
|
+
"maxLength": 500,
|
|
2123
|
+
"description": "What you are looking for, in natural language. Platform hints (\"on Reddit\", \"papers\") and time hints (\"this week\") steer engine and time-range selection."
|
|
2124
|
+
}
|
|
2125
|
+
},
|
|
2126
|
+
"required": [
|
|
2127
|
+
"query"
|
|
2128
|
+
],
|
|
2129
|
+
"additionalProperties": false
|
|
2130
|
+
},
|
|
2131
|
+
"examples": {
|
|
2132
|
+
"community": {
|
|
2133
|
+
"summary": "Let Search1API choose",
|
|
2134
|
+
"description": "Engines and the time window are inferred from the wording: community discussion, within the past month.",
|
|
2135
|
+
"value": {
|
|
2136
|
+
"query": "What are developers saying about Bun 1.3 this month?"
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
1672
2145
|
"/crawl": {
|
|
1673
2146
|
"post": {
|
|
1674
2147
|
"operationId": "crawl",
|
|
1675
2148
|
"summary": "Crawl a URL and extract its content",
|
|
1676
|
-
"description": "Fetch one public URL and return its readable title and body as clean text, with navigation, boilerplate, and scripts stripped. Use it on a URL the user supplied or one returned by POST /search. To ingest a whole site rather than a single page, use POST /deepcrawl. Costs 1 credit per request.",
|
|
2149
|
+
"description": "Fetch one public URL and return its readable title and body as clean text, with navigation, boilerplate, and scripts stripped. Use it on a URL the user supplied or one returned by POST /search. To ingest a whole site rather than a single page, use POST /deepcrawl. Send an array of request objects to crawl several URLs in one call: the response is an array containing only the URLs that succeeded, in no guaranteed order, so match items back to your input by `crawlParameters.url`; failed URLs are omitted and not charged. `enable_fallback` is accepted as a snake_case alias of `enableFallback`. Costs 1 credit per request.",
|
|
1677
2150
|
"tags": [
|
|
1678
2151
|
"Crawl"
|
|
1679
2152
|
],
|
|
@@ -1742,6 +2215,26 @@
|
|
|
1742
2215
|
}
|
|
1743
2216
|
}
|
|
1744
2217
|
},
|
|
2218
|
+
"404": {
|
|
2219
|
+
"description": "Not Found",
|
|
2220
|
+
"content": {
|
|
2221
|
+
"application/json": {
|
|
2222
|
+
"schema": {
|
|
2223
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
},
|
|
2228
|
+
"410": {
|
|
2229
|
+
"description": "Gone",
|
|
2230
|
+
"content": {
|
|
2231
|
+
"application/json": {
|
|
2232
|
+
"schema": {
|
|
2233
|
+
"$ref": "#/components/schemas/ApiError"
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
},
|
|
1745
2238
|
"422": {
|
|
1746
2239
|
"description": "Validation Error",
|
|
1747
2240
|
"content": {
|
|
@@ -1852,7 +2345,7 @@
|
|
|
1852
2345
|
"post": {
|
|
1853
2346
|
"operationId": "sitemap",
|
|
1854
2347
|
"summary": "Extract sitemap URLs from a website",
|
|
1855
|
-
"description": "Discover the public URLs of a site when you need to know which pages exist before fetching any of them — scoping a crawl, auditing coverage, or locating a section.
|
|
2348
|
+
"description": "Discover the public URLs of a site when you need to know which pages exist before fetching any of them — scoping a crawl, auditing coverage, or locating a section. With the default `type: \"sitemap\"` this reads the site's own sitemap: `Sitemap:` directives in robots.txt first, then the conventional paths, following sitemap index files and gzipped sitemaps, and returns the URLs it declares for the requested host. A site that publishes no sitemap falls back to the links found on the given page. Use `type: \"all\"` to skip the sitemap and get every link on that one page instead, including links to other hosts. Neither mode fetches page content. Costs 1 credit per request.",
|
|
1856
2349
|
"tags": [
|
|
1857
2350
|
"Crawl"
|
|
1858
2351
|
],
|
|
@@ -21,6 +21,7 @@ from .errors import (
|
|
|
21
21
|
api_status_error,
|
|
22
22
|
)
|
|
23
23
|
from .types import (
|
|
24
|
+
AskResponse,
|
|
24
25
|
BatchResponse,
|
|
25
26
|
CrawlRequest,
|
|
26
27
|
CrawlResponse,
|
|
@@ -28,6 +29,9 @@ from .types import (
|
|
|
28
29
|
DeepcrawlAcceptedResponse,
|
|
29
30
|
DeepcrawlStatusResponse,
|
|
30
31
|
ExtractResponse,
|
|
32
|
+
FeedbackAgent,
|
|
33
|
+
FeedbackCategory,
|
|
34
|
+
FeedbackResponse,
|
|
31
35
|
HealthResponse,
|
|
32
36
|
NewsEngine,
|
|
33
37
|
NewsRequest,
|
|
@@ -47,6 +51,9 @@ from .types import (
|
|
|
47
51
|
|
|
48
52
|
DEFAULT_BASE_URL = "https://api.search1api.com"
|
|
49
53
|
DEFAULT_TIMEOUT = 30.0
|
|
54
|
+
# The gateway gives Ask up to 35 seconds, so the default 30 seconds would abort
|
|
55
|
+
# requests the API would still answer.
|
|
56
|
+
DEFAULT_ASK_TIMEOUT = 45.0
|
|
50
57
|
DEFAULT_MAX_RETRIES = 2
|
|
51
58
|
DEFAULT_RETRY_DELAY = 0.5
|
|
52
59
|
DEFAULT_DEEPCRAWL_POLL_INTERVAL = 2.0
|
|
@@ -62,6 +69,7 @@ def _search_payload(
|
|
|
62
69
|
*,
|
|
63
70
|
search_service: Optional[str] = None,
|
|
64
71
|
max_results: Optional[int] = None,
|
|
72
|
+
page: Optional[int] = None,
|
|
65
73
|
crawl_results: Optional[int] = None,
|
|
66
74
|
image: Optional[bool] = None,
|
|
67
75
|
include_sites: Optional[List[str]] = None,
|
|
@@ -74,6 +82,7 @@ def _search_payload(
|
|
|
74
82
|
"query": query,
|
|
75
83
|
"search_service": search_service,
|
|
76
84
|
"max_results": max_results,
|
|
85
|
+
"page": page,
|
|
77
86
|
"crawl_results": crawl_results,
|
|
78
87
|
"image": image,
|
|
79
88
|
"include_sites": include_sites,
|
|
@@ -84,6 +93,25 @@ def _search_payload(
|
|
|
84
93
|
)
|
|
85
94
|
|
|
86
95
|
|
|
96
|
+
def _feedback_payload(
|
|
97
|
+
message: str,
|
|
98
|
+
*,
|
|
99
|
+
intent: Optional[str] = None,
|
|
100
|
+
category: Optional[FeedbackCategory] = None,
|
|
101
|
+
request_id: Optional[str] = None,
|
|
102
|
+
agent: Optional[FeedbackAgent] = None,
|
|
103
|
+
) -> Dict[str, Any]:
|
|
104
|
+
return _compact(
|
|
105
|
+
{
|
|
106
|
+
"message": message,
|
|
107
|
+
"intent": intent,
|
|
108
|
+
"category": category,
|
|
109
|
+
"request_id": request_id,
|
|
110
|
+
"agent": _compact(cast(Mapping[str, Any], agent)) if agent else None,
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
87
115
|
def _screenshot_payload(
|
|
88
116
|
url: str,
|
|
89
117
|
*,
|
|
@@ -179,7 +207,7 @@ class _ClientConfig:
|
|
|
179
207
|
return {
|
|
180
208
|
"Accept": accept,
|
|
181
209
|
"Authorization": f"Bearer {self.api_key}",
|
|
182
|
-
"X-Search1API-Client": "python/0.
|
|
210
|
+
"X-Search1API-Client": "python/0.3.0",
|
|
183
211
|
**self.headers,
|
|
184
212
|
}
|
|
185
213
|
|
|
@@ -229,6 +257,7 @@ class Search1API(_ClientConfig):
|
|
|
229
257
|
*,
|
|
230
258
|
search_service: Optional[SearchEngine] = None,
|
|
231
259
|
max_results: Optional[int] = None,
|
|
260
|
+
page: Optional[int] = None,
|
|
232
261
|
crawl_results: Optional[int] = None,
|
|
233
262
|
image: Optional[bool] = None,
|
|
234
263
|
include_sites: Optional[List[str]] = None,
|
|
@@ -240,6 +269,7 @@ class Search1API(_ClientConfig):
|
|
|
240
269
|
query,
|
|
241
270
|
search_service=search_service,
|
|
242
271
|
max_results=max_results,
|
|
272
|
+
page=page,
|
|
243
273
|
crawl_results=crawl_results,
|
|
244
274
|
image=image,
|
|
245
275
|
include_sites=include_sites,
|
|
@@ -281,6 +311,16 @@ class Search1API(_ClientConfig):
|
|
|
281
311
|
def news_batch(self, requests: List[NewsRequest]) -> BatchResponse:
|
|
282
312
|
return cast(BatchResponse, self._request_json("POST", "/news", json=requests))
|
|
283
313
|
|
|
314
|
+
def ask(self, query: str, *, timeout: float = DEFAULT_ASK_TIMEOUT) -> AskResponse:
|
|
315
|
+
"""Agentic search: Search1API picks the engines and time window.
|
|
316
|
+
|
|
317
|
+
A completed request costs 5 credits.
|
|
318
|
+
"""
|
|
319
|
+
return cast(
|
|
320
|
+
AskResponse,
|
|
321
|
+
self._request_json("POST", "/ask", json={"query": query}, timeout=timeout),
|
|
322
|
+
)
|
|
323
|
+
|
|
284
324
|
def crawl(
|
|
285
325
|
self, url: str, *, enable_fallback: Optional[bool] = None
|
|
286
326
|
) -> CrawlResponse:
|
|
@@ -442,6 +482,32 @@ class Search1API(_ClientConfig):
|
|
|
442
482
|
def health(self) -> HealthResponse:
|
|
443
483
|
return cast(HealthResponse, self._request_json("GET", "/health"))
|
|
444
484
|
|
|
485
|
+
def feedback(
|
|
486
|
+
self,
|
|
487
|
+
message: str,
|
|
488
|
+
*,
|
|
489
|
+
intent: Optional[str] = None,
|
|
490
|
+
category: Optional[FeedbackCategory] = None,
|
|
491
|
+
request_id: Optional[str] = None,
|
|
492
|
+
agent: Optional[FeedbackAgent] = None,
|
|
493
|
+
) -> FeedbackResponse:
|
|
494
|
+
"""Report a Search1API problem or missing capability. Free.
|
|
495
|
+
|
|
496
|
+
Never retried automatically: the API has no idempotency key, so a retry
|
|
497
|
+
after a lost response would file a duplicate report.
|
|
498
|
+
"""
|
|
499
|
+
payload = _feedback_payload(
|
|
500
|
+
message,
|
|
501
|
+
intent=intent,
|
|
502
|
+
category=category,
|
|
503
|
+
request_id=request_id,
|
|
504
|
+
agent=agent,
|
|
505
|
+
)
|
|
506
|
+
return cast(
|
|
507
|
+
FeedbackResponse,
|
|
508
|
+
self._request_json("POST", "/feedback", json=payload, retryable=False),
|
|
509
|
+
)
|
|
510
|
+
|
|
445
511
|
def _request_json(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
446
512
|
response = self._request(method, path, **kwargs)
|
|
447
513
|
try:
|
|
@@ -454,6 +520,7 @@ class Search1API(_ClientConfig):
|
|
|
454
520
|
def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
455
521
|
retryable = bool(kwargs.pop("retryable", True))
|
|
456
522
|
accept = str(kwargs.pop("accept", "application/json"))
|
|
523
|
+
timeout = float(kwargs.pop("timeout", self.timeout))
|
|
457
524
|
max_retries = self.max_retries if retryable else 0
|
|
458
525
|
for attempt in range(max_retries + 1):
|
|
459
526
|
try:
|
|
@@ -461,7 +528,7 @@ class Search1API(_ClientConfig):
|
|
|
461
528
|
method,
|
|
462
529
|
f"{self.base_url}{path}",
|
|
463
530
|
headers=self._request_headers(accept),
|
|
464
|
-
timeout=
|
|
531
|
+
timeout=timeout,
|
|
465
532
|
**kwargs,
|
|
466
533
|
)
|
|
467
534
|
except httpx.TimeoutException as exc:
|
|
@@ -469,7 +536,7 @@ class Search1API(_ClientConfig):
|
|
|
469
536
|
time.sleep(self._retry_sleep(attempt))
|
|
470
537
|
continue
|
|
471
538
|
raise APITimeoutError(
|
|
472
|
-
f"Search1API request timed out after {
|
|
539
|
+
f"Search1API request timed out after {timeout:g}s"
|
|
473
540
|
) from exc
|
|
474
541
|
except httpx.RequestError as exc:
|
|
475
542
|
if attempt < max_retries:
|
|
@@ -530,6 +597,7 @@ class AsyncSearch1API(_ClientConfig):
|
|
|
530
597
|
*,
|
|
531
598
|
search_service: Optional[SearchEngine] = None,
|
|
532
599
|
max_results: Optional[int] = None,
|
|
600
|
+
page: Optional[int] = None,
|
|
533
601
|
crawl_results: Optional[int] = None,
|
|
534
602
|
image: Optional[bool] = None,
|
|
535
603
|
include_sites: Optional[List[str]] = None,
|
|
@@ -546,6 +614,7 @@ class AsyncSearch1API(_ClientConfig):
|
|
|
546
614
|
query,
|
|
547
615
|
search_service=search_service,
|
|
548
616
|
max_results=max_results,
|
|
617
|
+
page=page,
|
|
549
618
|
crawl_results=crawl_results,
|
|
550
619
|
image=image,
|
|
551
620
|
include_sites=include_sites,
|
|
@@ -600,6 +669,20 @@ class AsyncSearch1API(_ClientConfig):
|
|
|
600
669
|
await self._request_json("POST", "/news", json=requests),
|
|
601
670
|
)
|
|
602
671
|
|
|
672
|
+
async def ask(
|
|
673
|
+
self, query: str, *, timeout: float = DEFAULT_ASK_TIMEOUT
|
|
674
|
+
) -> AskResponse:
|
|
675
|
+
"""Agentic search: Search1API picks the engines and time window.
|
|
676
|
+
|
|
677
|
+
A completed request costs 5 credits.
|
|
678
|
+
"""
|
|
679
|
+
return cast(
|
|
680
|
+
AskResponse,
|
|
681
|
+
await self._request_json(
|
|
682
|
+
"POST", "/ask", json={"query": query}, timeout=timeout
|
|
683
|
+
),
|
|
684
|
+
)
|
|
685
|
+
|
|
603
686
|
async def crawl(
|
|
604
687
|
self, url: str, *, enable_fallback: Optional[bool] = None
|
|
605
688
|
) -> CrawlResponse:
|
|
@@ -775,6 +858,34 @@ class AsyncSearch1API(_ClientConfig):
|
|
|
775
858
|
async def health(self) -> HealthResponse:
|
|
776
859
|
return cast(HealthResponse, await self._request_json("GET", "/health"))
|
|
777
860
|
|
|
861
|
+
async def feedback(
|
|
862
|
+
self,
|
|
863
|
+
message: str,
|
|
864
|
+
*,
|
|
865
|
+
intent: Optional[str] = None,
|
|
866
|
+
category: Optional[FeedbackCategory] = None,
|
|
867
|
+
request_id: Optional[str] = None,
|
|
868
|
+
agent: Optional[FeedbackAgent] = None,
|
|
869
|
+
) -> FeedbackResponse:
|
|
870
|
+
"""Report a Search1API problem or missing capability. Free.
|
|
871
|
+
|
|
872
|
+
Never retried automatically: the API has no idempotency key, so a retry
|
|
873
|
+
after a lost response would file a duplicate report.
|
|
874
|
+
"""
|
|
875
|
+
payload = _feedback_payload(
|
|
876
|
+
message,
|
|
877
|
+
intent=intent,
|
|
878
|
+
category=category,
|
|
879
|
+
request_id=request_id,
|
|
880
|
+
agent=agent,
|
|
881
|
+
)
|
|
882
|
+
return cast(
|
|
883
|
+
FeedbackResponse,
|
|
884
|
+
await self._request_json(
|
|
885
|
+
"POST", "/feedback", json=payload, retryable=False
|
|
886
|
+
),
|
|
887
|
+
)
|
|
888
|
+
|
|
778
889
|
async def _request_json(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
779
890
|
response = await self._request(method, path, **kwargs)
|
|
780
891
|
try:
|
|
@@ -787,6 +898,7 @@ class AsyncSearch1API(_ClientConfig):
|
|
|
787
898
|
async def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
788
899
|
retryable = bool(kwargs.pop("retryable", True))
|
|
789
900
|
accept = str(kwargs.pop("accept", "application/json"))
|
|
901
|
+
timeout = float(kwargs.pop("timeout", self.timeout))
|
|
790
902
|
max_retries = self.max_retries if retryable else 0
|
|
791
903
|
for attempt in range(max_retries + 1):
|
|
792
904
|
try:
|
|
@@ -794,7 +906,7 @@ class AsyncSearch1API(_ClientConfig):
|
|
|
794
906
|
method,
|
|
795
907
|
f"{self.base_url}{path}",
|
|
796
908
|
headers=self._request_headers(accept),
|
|
797
|
-
timeout=
|
|
909
|
+
timeout=timeout,
|
|
798
910
|
**kwargs,
|
|
799
911
|
)
|
|
800
912
|
except httpx.TimeoutException as exc:
|
|
@@ -802,7 +914,7 @@ class AsyncSearch1API(_ClientConfig):
|
|
|
802
914
|
await asyncio.sleep(self._retry_sleep(attempt))
|
|
803
915
|
continue
|
|
804
916
|
raise APITimeoutError(
|
|
805
|
-
f"Search1API request timed out after {
|
|
917
|
+
f"Search1API request timed out after {timeout:g}s"
|
|
806
918
|
) from exc
|
|
807
919
|
except httpx.RequestError as exc:
|
|
808
920
|
if attempt < max_retries:
|
|
@@ -8,8 +8,10 @@ TimeRange = Literal["day", "week", "month", "year"]
|
|
|
8
8
|
SearchEngine = Literal[
|
|
9
9
|
"google",
|
|
10
10
|
"bing",
|
|
11
|
+
"bingcn",
|
|
11
12
|
"duckduckgo",
|
|
12
13
|
"yahoo",
|
|
14
|
+
"yandex",
|
|
13
15
|
"youtube",
|
|
14
16
|
"x",
|
|
15
17
|
"reddit",
|
|
@@ -19,7 +21,7 @@ SearchEngine = Literal[
|
|
|
19
21
|
"bilibili",
|
|
20
22
|
"imdb",
|
|
21
23
|
"wikipedia",
|
|
22
|
-
"
|
|
24
|
+
"grokipedia",
|
|
23
25
|
"baidu",
|
|
24
26
|
"360",
|
|
25
27
|
"quark",
|
|
@@ -37,6 +39,7 @@ class SearchRequestRequired(TypedDict):
|
|
|
37
39
|
class SearchRequest(SearchRequestRequired, total=False):
|
|
38
40
|
search_service: SearchEngine
|
|
39
41
|
max_results: int
|
|
42
|
+
page: int
|
|
40
43
|
crawl_results: int
|
|
41
44
|
image: bool
|
|
42
45
|
include_sites: List[str]
|
|
@@ -53,6 +56,13 @@ class SearchResultRequired(TypedDict):
|
|
|
53
56
|
|
|
54
57
|
class SearchResult(SearchResultRequired, total=False):
|
|
55
58
|
content: str
|
|
59
|
+
published_date: str
|
|
60
|
+
kind: Literal["repo", "issue", "pr", "discussion"]
|
|
61
|
+
stars: int
|
|
62
|
+
language: str
|
|
63
|
+
num_comments: int
|
|
64
|
+
points: int
|
|
65
|
+
story_url: str
|
|
56
66
|
|
|
57
67
|
|
|
58
68
|
class SearchResponseRequired(TypedDict):
|
|
@@ -82,6 +92,49 @@ class NewsRequest(NewsRequestRequired, total=False):
|
|
|
82
92
|
time_range: TimeRange
|
|
83
93
|
|
|
84
94
|
|
|
95
|
+
class AskIntent(TypedDict):
|
|
96
|
+
search_query: str
|
|
97
|
+
sources: List[str]
|
|
98
|
+
time_range: Optional[TimeRange]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AskResultRequired(TypedDict):
|
|
102
|
+
title: str
|
|
103
|
+
link: str
|
|
104
|
+
snippet: str
|
|
105
|
+
source: str
|
|
106
|
+
relevance: float
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AskResult(AskResultRequired, total=False):
|
|
110
|
+
published_date: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AskError(TypedDict):
|
|
114
|
+
source: str
|
|
115
|
+
message: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AskResponse(TypedDict):
|
|
119
|
+
query: str
|
|
120
|
+
intent: AskIntent
|
|
121
|
+
results: List[AskResult]
|
|
122
|
+
errors: List[AskError]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
FeedbackCategory = Literal["bug", "feature_request", "docs", "other"]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class FeedbackAgent(TypedDict, total=False):
|
|
129
|
+
name: str
|
|
130
|
+
model: str
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class FeedbackResponse(TypedDict):
|
|
134
|
+
id: str
|
|
135
|
+
status: Literal["new"]
|
|
136
|
+
|
|
137
|
+
|
|
85
138
|
class BatchItemRequired(TypedDict):
|
|
86
139
|
success: bool
|
|
87
140
|
cost: int
|
|
@@ -215,6 +215,123 @@ def test_deepcrawl_task_creation_is_not_retried():
|
|
|
215
215
|
http_client.close()
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
def test_search_sends_page_and_new_engines():
|
|
219
|
+
bodies = []
|
|
220
|
+
|
|
221
|
+
def handler(request):
|
|
222
|
+
bodies.append(json.loads(request.content))
|
|
223
|
+
return json_response(200, {"results": []}, request)
|
|
224
|
+
|
|
225
|
+
http_client = httpx.Client(transport=httpx.MockTransport(handler))
|
|
226
|
+
client = Search1API("test-key", client=http_client)
|
|
227
|
+
|
|
228
|
+
client.search("kubernetes", search_service="bingcn", page=2)
|
|
229
|
+
client.search("Alan Turing", search_service="grokipedia")
|
|
230
|
+
|
|
231
|
+
assert bodies == [
|
|
232
|
+
{"query": "kubernetes", "search_service": "bingcn", "page": 2},
|
|
233
|
+
{"query": "Alan Turing", "search_service": "grokipedia"},
|
|
234
|
+
]
|
|
235
|
+
http_client.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_ask_sends_only_the_query_with_a_longer_timeout():
|
|
239
|
+
seen = []
|
|
240
|
+
body = {
|
|
241
|
+
"query": "recent papers on speculative decoding",
|
|
242
|
+
"intent": {
|
|
243
|
+
"search_query": "speculative decoding",
|
|
244
|
+
"sources": ["arxiv"],
|
|
245
|
+
"time_range": None,
|
|
246
|
+
},
|
|
247
|
+
"results": [
|
|
248
|
+
{
|
|
249
|
+
"title": "Speculative decoding survey",
|
|
250
|
+
"link": "https://arxiv.org/abs/1",
|
|
251
|
+
"snippet": "Survey",
|
|
252
|
+
"source": "arxiv",
|
|
253
|
+
"relevance": 0.88,
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
"errors": [],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
def handler(request):
|
|
260
|
+
seen.append(request)
|
|
261
|
+
return json_response(200, body, request)
|
|
262
|
+
|
|
263
|
+
http_client = httpx.Client(transport=httpx.MockTransport(handler))
|
|
264
|
+
client = Search1API("test-key", client=http_client)
|
|
265
|
+
|
|
266
|
+
assert client.ask(body["query"]) == body
|
|
267
|
+
request = seen[0]
|
|
268
|
+
assert request.method == "POST"
|
|
269
|
+
assert str(request.url) == "https://api.search1api.com/ask"
|
|
270
|
+
assert json.loads(request.content) == {"query": body["query"]}
|
|
271
|
+
assert request.extensions["timeout"]["read"] == 45.0
|
|
272
|
+
http_client.close()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_feedback_is_sent_once_without_retries():
|
|
276
|
+
calls = []
|
|
277
|
+
|
|
278
|
+
def handler(request):
|
|
279
|
+
calls.append(json.loads(request.content))
|
|
280
|
+
return json_response(503, {"ok": False, "message": "unavailable"}, request)
|
|
281
|
+
|
|
282
|
+
http_client = httpx.Client(transport=httpx.MockTransport(handler))
|
|
283
|
+
client = Search1API("test-key", client=http_client, max_retries=2, retry_delay=0)
|
|
284
|
+
|
|
285
|
+
with pytest.raises(InternalServerError, match="unavailable"):
|
|
286
|
+
client.feedback(
|
|
287
|
+
"Need publication dates",
|
|
288
|
+
category="feature_request",
|
|
289
|
+
request_id="req_1",
|
|
290
|
+
agent={"name": "codex"},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
assert calls == [
|
|
294
|
+
{
|
|
295
|
+
"message": "Need publication dates",
|
|
296
|
+
"category": "feature_request",
|
|
297
|
+
"request_id": "req_1",
|
|
298
|
+
"agent": {"name": "codex"},
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
http_client.close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_async_ask_and_feedback():
|
|
305
|
+
async def run():
|
|
306
|
+
paths = []
|
|
307
|
+
|
|
308
|
+
async def handler(request):
|
|
309
|
+
paths.append(request.url.path)
|
|
310
|
+
if request.url.path == "/feedback":
|
|
311
|
+
return json_response(201, {"id": "fb_1", "status": "new"}, request)
|
|
312
|
+
return json_response(
|
|
313
|
+
200,
|
|
314
|
+
{
|
|
315
|
+
"query": "q",
|
|
316
|
+
"intent": {"search_query": "q", "sources": [], "time_range": None},
|
|
317
|
+
"results": [],
|
|
318
|
+
"errors": [],
|
|
319
|
+
},
|
|
320
|
+
request,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
http_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
|
324
|
+
client = AsyncSearch1API("test-key", client=http_client)
|
|
325
|
+
try:
|
|
326
|
+
assert (await client.ask("q"))["results"] == []
|
|
327
|
+
assert await client.feedback("docs typo") == {"id": "fb_1", "status": "new"}
|
|
328
|
+
finally:
|
|
329
|
+
await http_client.aclose()
|
|
330
|
+
assert paths == ["/ask", "/feedback"]
|
|
331
|
+
|
|
332
|
+
asyncio.run(run())
|
|
333
|
+
|
|
334
|
+
|
|
218
335
|
def test_async_client_uses_the_same_api_shape():
|
|
219
336
|
async def run():
|
|
220
337
|
async def handler(request):
|
|
@@ -252,10 +369,12 @@ def test_sdk_covers_every_public_openapi_operation():
|
|
|
252
369
|
)
|
|
253
370
|
|
|
254
371
|
operation_methods = {
|
|
372
|
+
"ask": "ask",
|
|
255
373
|
"crawl": "crawl",
|
|
256
374
|
"deepcrawl": "start_deepcrawl",
|
|
257
375
|
"deepcrawlStatus": "get_deepcrawl_status",
|
|
258
376
|
"extract": "extract",
|
|
377
|
+
"feedback": "feedback",
|
|
259
378
|
"health": "health",
|
|
260
379
|
"news": "news",
|
|
261
380
|
"search": "search",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|