exa-py 1.8.8__tar.gz → 1.8.9__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.
Potentially problematic release.
This version of exa-py might be problematic. Click here for more details.
- {exa_py-1.8.8 → exa_py-1.8.9}/PKG-INFO +13 -11
- {exa_py-1.8.8 → exa_py-1.8.9}/exa_py/api.py +151 -98
- exa_py-1.8.9/exa_py.egg-info/PKG-INFO +108 -0
- exa_py-1.8.9/exa_py.egg-info/SOURCES.txt +12 -0
- exa_py-1.8.9/exa_py.egg-info/dependency_links.txt +1 -0
- exa_py-1.8.9/exa_py.egg-info/requires.txt +3 -0
- exa_py-1.8.9/exa_py.egg-info/top_level.txt +1 -0
- exa_py-1.8.9/setup.cfg +4 -0
- exa_py-1.8.9/setup.py +26 -0
- {exa_py-1.8.8 → exa_py-1.8.9}/README.md +0 -0
- {exa_py-1.8.8 → exa_py-1.8.9}/exa_py/__init__.py +0 -0
- {exa_py-1.8.8 → exa_py-1.8.9}/exa_py/py.typed +0 -0
- {exa_py-1.8.8 → exa_py-1.8.9}/exa_py/utils.py +0 -0
- {exa_py-1.8.8 → exa_py-1.8.9}/pyproject.toml +0 -0
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name:
|
|
3
|
-
Version: 1.8.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: exa_py
|
|
3
|
+
Version: 1.8.9
|
|
4
4
|
Summary: Python SDK for Exa API.
|
|
5
|
-
|
|
5
|
+
Home-page: https://github.com/exa-labs/exa-py
|
|
6
|
+
Author: Exa
|
|
6
7
|
Author-email: hello@exa.ai
|
|
7
|
-
|
|
8
|
-
Classifier:
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
10
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
-
Requires-Dist: openai (>=1.48,<2.0)
|
|
15
|
-
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
16
|
-
Requires-Dist: typing-extensions (>=4.12.2,<5.0.0)
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Requires-Dist: typing-extensions
|
|
20
|
+
Requires-Dist: openai>=1.10.0
|
|
18
21
|
|
|
19
22
|
# Exa
|
|
20
23
|
|
|
@@ -103,4 +106,3 @@ exa = Exa(api_key="your-api-key")
|
|
|
103
106
|
|
|
104
107
|
```
|
|
105
108
|
|
|
106
|
-
|
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import dataclasses
|
|
4
|
-
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
|
-
import
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import wraps
|
|
7
9
|
from typing import (
|
|
8
10
|
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
Generic,
|
|
9
13
|
Iterable,
|
|
14
|
+
Iterator,
|
|
10
15
|
List,
|
|
16
|
+
Literal,
|
|
11
17
|
Optional,
|
|
12
|
-
Dict,
|
|
13
|
-
Generic,
|
|
14
18
|
TypeVar,
|
|
15
|
-
overload,
|
|
16
19
|
Union,
|
|
17
|
-
Literal,
|
|
18
|
-
get_origin,
|
|
19
20
|
get_args,
|
|
20
|
-
|
|
21
|
+
get_origin,
|
|
22
|
+
overload,
|
|
21
23
|
)
|
|
22
|
-
from typing_extensions import TypedDict
|
|
23
|
-
import json
|
|
24
24
|
|
|
25
|
+
import requests
|
|
25
26
|
from openai import OpenAI
|
|
26
27
|
from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam
|
|
27
28
|
from openai.types.chat_model import ChatModel
|
|
29
|
+
from typing_extensions import TypedDict
|
|
30
|
+
|
|
28
31
|
from exa_py.utils import (
|
|
29
32
|
ExaOpenAICompletion,
|
|
30
33
|
add_message_to_messages,
|
|
31
34
|
format_exa_result,
|
|
32
35
|
maybe_get_query,
|
|
33
36
|
)
|
|
34
|
-
import os
|
|
35
37
|
|
|
36
38
|
is_beta = os.getenv("IS_BETA") == "True"
|
|
37
39
|
|
|
@@ -102,17 +104,29 @@ def to_snake_case(data: dict) -> dict:
|
|
|
102
104
|
SEARCH_OPTIONS_TYPES = {
|
|
103
105
|
"query": [str], # The query string.
|
|
104
106
|
"num_results": [int], # Number of results (Default: 10, Max for basic: 10).
|
|
105
|
-
"include_domains": [
|
|
107
|
+
"include_domains": [
|
|
108
|
+
list
|
|
109
|
+
], # Domains to search from; exclusive with 'exclude_domains'.
|
|
106
110
|
"exclude_domains": [list], # Domains to omit; exclusive with 'include_domains'.
|
|
107
111
|
"start_crawl_date": [str], # Results after this crawl date. ISO 8601 format.
|
|
108
112
|
"end_crawl_date": [str], # Results before this crawl date. ISO 8601 format.
|
|
109
|
-
"start_published_date": [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
+
"start_published_date": [
|
|
114
|
+
str
|
|
115
|
+
], # Results after this publish date; excludes links with no date. ISO 8601 format.
|
|
116
|
+
"end_published_date": [
|
|
117
|
+
str
|
|
118
|
+
], # Results before this publish date; excludes links with no date. ISO 8601 format.
|
|
119
|
+
"include_text": [
|
|
120
|
+
list
|
|
121
|
+
], # Must be present in webpage text. (One string, up to 5 words)
|
|
122
|
+
"exclude_text": [
|
|
123
|
+
list
|
|
124
|
+
], # Must not be present in webpage text. (One string, up to 5 words)
|
|
113
125
|
"use_autoprompt": [bool], # Convert query to Exa. (Default: false)
|
|
114
126
|
"type": [str], # 'keyword', 'neural', or 'auto' (Default: auto)
|
|
115
|
-
"category": [
|
|
127
|
+
"category": [
|
|
128
|
+
str
|
|
129
|
+
], # A data category to focus on: 'company', 'research paper', 'news', 'pdf', 'github', 'tweet', 'personal site', 'linkedin profile', 'financial report'
|
|
116
130
|
"flags": [list], # Experimental flags array for Exa usage.
|
|
117
131
|
"moderation": [bool], # If true, moderate search results for safety.
|
|
118
132
|
}
|
|
@@ -188,6 +202,25 @@ def is_valid_type(value, expected_type):
|
|
|
188
202
|
return False # For any other case
|
|
189
203
|
|
|
190
204
|
|
|
205
|
+
def parse_cost_dollars(raw: dict) -> Optional[CostDollars]:
|
|
206
|
+
"""
|
|
207
|
+
Parse the costDollars JSON into a CostDollars object, or return None if missing/invalid.
|
|
208
|
+
"""
|
|
209
|
+
if not raw:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
total = raw.get("total")
|
|
213
|
+
if total is None:
|
|
214
|
+
# If there's no total, treat as absent
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# search and contents can be dictionaries or None
|
|
218
|
+
search_part = raw.get("search")
|
|
219
|
+
contents_part = raw.get("contents")
|
|
220
|
+
|
|
221
|
+
return CostDollars(total=total, search=search_part, contents=contents_part)
|
|
222
|
+
|
|
223
|
+
|
|
191
224
|
class TextContentsOptions(TypedDict, total=False):
|
|
192
225
|
"""A class representing the options that you can specify when requesting text
|
|
193
226
|
|
|
@@ -231,6 +264,30 @@ class ExtrasOptions(TypedDict, total=False):
|
|
|
231
264
|
image_links: int
|
|
232
265
|
|
|
233
266
|
|
|
267
|
+
class CostDollarsSearch(TypedDict, total=False):
|
|
268
|
+
"""Represents the cost breakdown for search."""
|
|
269
|
+
|
|
270
|
+
neural: float
|
|
271
|
+
keyword: float
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class CostDollarsContents(TypedDict, total=False):
|
|
275
|
+
"""Represents the cost breakdown for contents."""
|
|
276
|
+
|
|
277
|
+
text: float
|
|
278
|
+
highlights: float
|
|
279
|
+
summary: float
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class CostDollars:
|
|
284
|
+
"""Represents costDollars field in the API response."""
|
|
285
|
+
|
|
286
|
+
total: float
|
|
287
|
+
search: CostDollarsSearch = None
|
|
288
|
+
contents: CostDollarsContents = None
|
|
289
|
+
|
|
290
|
+
|
|
234
291
|
@dataclass
|
|
235
292
|
class _Result:
|
|
236
293
|
"""A class representing the base fields of a search result.
|
|
@@ -515,7 +572,8 @@ class AnswerResult:
|
|
|
515
572
|
author (str, optional): If available, the author of the content.
|
|
516
573
|
text (str, optional): The full page text from each search result.
|
|
517
574
|
"""
|
|
518
|
-
|
|
575
|
+
|
|
576
|
+
id: str
|
|
519
577
|
url: str
|
|
520
578
|
title: Optional[str] = None
|
|
521
579
|
published_date: Optional[str] = None
|
|
@@ -523,12 +581,12 @@ class AnswerResult:
|
|
|
523
581
|
text: Optional[str] = None
|
|
524
582
|
|
|
525
583
|
def __init__(self, **kwargs):
|
|
526
|
-
self.id = kwargs[
|
|
527
|
-
self.url = kwargs[
|
|
528
|
-
self.title = kwargs.get(
|
|
529
|
-
self.published_date = kwargs.get(
|
|
530
|
-
self.author = kwargs.get(
|
|
531
|
-
self.text = kwargs.get(
|
|
584
|
+
self.id = kwargs["id"]
|
|
585
|
+
self.url = kwargs["url"]
|
|
586
|
+
self.title = kwargs.get("title")
|
|
587
|
+
self.published_date = kwargs.get("published_date")
|
|
588
|
+
self.author = kwargs.get("author")
|
|
589
|
+
self.text = kwargs.get("text")
|
|
532
590
|
|
|
533
591
|
def __str__(self):
|
|
534
592
|
return (
|
|
@@ -539,18 +597,20 @@ class AnswerResult:
|
|
|
539
597
|
f"Author: {self.author}\n"
|
|
540
598
|
f"Text: {self.text}\n\n"
|
|
541
599
|
)
|
|
542
|
-
|
|
600
|
+
|
|
601
|
+
|
|
543
602
|
@dataclass
|
|
544
603
|
class StreamChunk:
|
|
545
604
|
"""A class representing a single chunk of streaming data.
|
|
546
|
-
|
|
605
|
+
|
|
547
606
|
Attributes:
|
|
548
607
|
content (Optional[str]): The partial text content of the answer
|
|
549
608
|
citations (Optional[List[AnswerResult]]): List of citations if provided in this chunk
|
|
550
609
|
"""
|
|
610
|
+
|
|
551
611
|
content: Optional[str] = None
|
|
552
612
|
citations: Optional[List[AnswerResult]] = None
|
|
553
|
-
|
|
613
|
+
|
|
554
614
|
def has_data(self) -> bool:
|
|
555
615
|
"""Check if this chunk contains any data."""
|
|
556
616
|
return self.content is not None or self.citations is not None
|
|
@@ -594,6 +654,7 @@ class AnswerResponse:
|
|
|
594
654
|
|
|
595
655
|
class StreamAnswerResponse:
|
|
596
656
|
"""A class representing a streaming answer response."""
|
|
657
|
+
|
|
597
658
|
def __init__(self, raw_response: requests.Response):
|
|
598
659
|
self._raw_response = raw_response
|
|
599
660
|
self._ensure_ok_status()
|
|
@@ -621,8 +682,14 @@ class StreamAnswerResponse:
|
|
|
621
682
|
if "delta" in chunk["choices"][0]:
|
|
622
683
|
content = chunk["choices"][0]["delta"].get("content")
|
|
623
684
|
|
|
624
|
-
if
|
|
625
|
-
citations
|
|
685
|
+
if (
|
|
686
|
+
"citations" in chunk
|
|
687
|
+
and chunk["citations"]
|
|
688
|
+
and chunk["citations"] != "null"
|
|
689
|
+
):
|
|
690
|
+
citations = [
|
|
691
|
+
AnswerResult(**to_snake_case(s)) for s in chunk["citations"]
|
|
692
|
+
]
|
|
626
693
|
|
|
627
694
|
stream_chunk = StreamChunk(content=content, citations=citations)
|
|
628
695
|
if stream_chunk.has_data():
|
|
@@ -651,6 +718,7 @@ class SearchResponse(Generic[T]):
|
|
|
651
718
|
autoprompt_string: Optional[str]
|
|
652
719
|
resolved_search_type: Optional[str]
|
|
653
720
|
auto_date: Optional[str]
|
|
721
|
+
cost_dollars: Optional[CostDollars] = None
|
|
654
722
|
|
|
655
723
|
def __str__(self):
|
|
656
724
|
output = "\n\n".join(str(result) for result in self.results)
|
|
@@ -658,7 +726,12 @@ class SearchResponse(Generic[T]):
|
|
|
658
726
|
output += f"\n\nAutoprompt String: {self.autoprompt_string}"
|
|
659
727
|
if self.resolved_search_type:
|
|
660
728
|
output += f"\nResolved Search Type: {self.resolved_search_type}"
|
|
661
|
-
|
|
729
|
+
if self.cost_dollars:
|
|
730
|
+
output += f"\nCostDollars: total={self.cost_dollars.total}"
|
|
731
|
+
if self.cost_dollars.search:
|
|
732
|
+
output += f"\n - search: {self.cost_dollars.search}"
|
|
733
|
+
if self.cost_dollars.contents:
|
|
734
|
+
output += f"\n - contents: {self.cost_dollars.contents}"
|
|
662
735
|
return output
|
|
663
736
|
|
|
664
737
|
|
|
@@ -686,7 +759,7 @@ class Exa:
|
|
|
686
759
|
self,
|
|
687
760
|
api_key: Optional[str],
|
|
688
761
|
base_url: str = "https://api.exa.ai",
|
|
689
|
-
user_agent: str = "exa-py 1.8.
|
|
762
|
+
user_agent: str = "exa-py 1.8.9",
|
|
690
763
|
):
|
|
691
764
|
"""Initialize the Exa client with the provided API key and optional base URL and user agent.
|
|
692
765
|
|
|
@@ -720,12 +793,16 @@ class Exa:
|
|
|
720
793
|
ValueError: If the request fails (non-200 status code).
|
|
721
794
|
"""
|
|
722
795
|
if data.get("stream"):
|
|
723
|
-
res = requests.post(
|
|
796
|
+
res = requests.post(
|
|
797
|
+
self.base_url + endpoint, json=data, headers=self.headers, stream=True
|
|
798
|
+
)
|
|
724
799
|
return res
|
|
725
800
|
|
|
726
801
|
res = requests.post(self.base_url + endpoint, json=data, headers=self.headers)
|
|
727
802
|
if res.status_code != 200:
|
|
728
|
-
raise ValueError(
|
|
803
|
+
raise ValueError(
|
|
804
|
+
f"Request failed with status code {res.status_code}: {res.text}"
|
|
805
|
+
)
|
|
729
806
|
return res.json()
|
|
730
807
|
|
|
731
808
|
def search(
|
|
@@ -773,11 +850,13 @@ class Exa:
|
|
|
773
850
|
validate_search_options(options, SEARCH_OPTIONS_TYPES)
|
|
774
851
|
options = to_camel_case(options)
|
|
775
852
|
data = self.request("/search", options)
|
|
853
|
+
cost_dollars = parse_cost_dollars(data.get("costDollars"))
|
|
776
854
|
return SearchResponse(
|
|
777
855
|
[Result(**to_snake_case(result)) for result in data["results"]],
|
|
778
856
|
data["autopromptString"] if "autopromptString" in data else None,
|
|
779
857
|
data["resolvedSearchType"] if "resolvedSearchType" in data else None,
|
|
780
858
|
data["autoDate"] if "autoDate" in data else None,
|
|
859
|
+
cost_dollars=cost_dollars,
|
|
781
860
|
)
|
|
782
861
|
|
|
783
862
|
@overload
|
|
@@ -805,8 +884,7 @@ class Exa:
|
|
|
805
884
|
subpages: Optional[int] = None,
|
|
806
885
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
807
886
|
extras: Optional[ExtrasOptions] = None,
|
|
808
|
-
) -> SearchResponse[ResultWithText]:
|
|
809
|
-
...
|
|
887
|
+
) -> SearchResponse[ResultWithText]: ...
|
|
810
888
|
|
|
811
889
|
@overload
|
|
812
890
|
def search_and_contents(
|
|
@@ -834,8 +912,7 @@ class Exa:
|
|
|
834
912
|
filter_empty_results: Optional[bool] = None,
|
|
835
913
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
836
914
|
extras: Optional[ExtrasOptions] = None,
|
|
837
|
-
) -> SearchResponse[ResultWithText]:
|
|
838
|
-
...
|
|
915
|
+
) -> SearchResponse[ResultWithText]: ...
|
|
839
916
|
|
|
840
917
|
@overload
|
|
841
918
|
def search_and_contents(
|
|
@@ -863,8 +940,7 @@ class Exa:
|
|
|
863
940
|
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
|
|
864
941
|
filter_empty_results: Optional[bool] = None,
|
|
865
942
|
extras: Optional[ExtrasOptions] = None,
|
|
866
|
-
) -> SearchResponse[ResultWithHighlights]:
|
|
867
|
-
...
|
|
943
|
+
) -> SearchResponse[ResultWithHighlights]: ...
|
|
868
944
|
|
|
869
945
|
@overload
|
|
870
946
|
def search_and_contents(
|
|
@@ -893,8 +969,7 @@ class Exa:
|
|
|
893
969
|
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
|
|
894
970
|
filter_empty_results: Optional[bool] = None,
|
|
895
971
|
extras: Optional[ExtrasOptions] = None,
|
|
896
|
-
) -> SearchResponse[ResultWithTextAndHighlights]:
|
|
897
|
-
...
|
|
972
|
+
) -> SearchResponse[ResultWithTextAndHighlights]: ...
|
|
898
973
|
|
|
899
974
|
@overload
|
|
900
975
|
def search_and_contents(
|
|
@@ -922,8 +997,7 @@ class Exa:
|
|
|
922
997
|
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
|
|
923
998
|
filter_empty_results: Optional[bool] = None,
|
|
924
999
|
extras: Optional[ExtrasOptions] = None,
|
|
925
|
-
) -> SearchResponse[ResultWithSummary]:
|
|
926
|
-
...
|
|
1000
|
+
) -> SearchResponse[ResultWithSummary]: ...
|
|
927
1001
|
|
|
928
1002
|
@overload
|
|
929
1003
|
def search_and_contents(
|
|
@@ -952,8 +1026,7 @@ class Exa:
|
|
|
952
1026
|
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
|
|
953
1027
|
filter_empty_results: Optional[bool] = None,
|
|
954
1028
|
extras: Optional[ExtrasOptions] = None,
|
|
955
|
-
) -> SearchResponse[ResultWithTextAndSummary]:
|
|
956
|
-
...
|
|
1029
|
+
) -> SearchResponse[ResultWithTextAndSummary]: ...
|
|
957
1030
|
|
|
958
1031
|
@overload
|
|
959
1032
|
def search_and_contents(
|
|
@@ -982,8 +1055,7 @@ class Exa:
|
|
|
982
1055
|
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
|
|
983
1056
|
filter_empty_results: Optional[bool] = None,
|
|
984
1057
|
extras: Optional[ExtrasOptions] = None,
|
|
985
|
-
) -> SearchResponse[ResultWithHighlightsAndSummary]:
|
|
986
|
-
...
|
|
1058
|
+
) -> SearchResponse[ResultWithHighlightsAndSummary]: ...
|
|
987
1059
|
|
|
988
1060
|
@overload
|
|
989
1061
|
def search_and_contents(
|
|
@@ -1013,8 +1085,7 @@ class Exa:
|
|
|
1013
1085
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1014
1086
|
filter_empty_results: Optional[bool] = None,
|
|
1015
1087
|
extras: Optional[ExtrasOptions] = None,
|
|
1016
|
-
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
|
|
1017
|
-
...
|
|
1088
|
+
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...
|
|
1018
1089
|
|
|
1019
1090
|
def search_and_contents(self, query: str, **kwargs):
|
|
1020
1091
|
options = {k: v for k, v in {"query": query, **kwargs}.items() if v is not None}
|
|
@@ -1053,11 +1124,13 @@ class Exa:
|
|
|
1053
1124
|
)
|
|
1054
1125
|
options = to_camel_case(options)
|
|
1055
1126
|
data = self.request("/search", options)
|
|
1127
|
+
cost_dollars = parse_cost_dollars(data.get("costDollars"))
|
|
1056
1128
|
return SearchResponse(
|
|
1057
1129
|
[Result(**to_snake_case(result)) for result in data["results"]],
|
|
1058
1130
|
data["autopromptString"] if "autopromptString" in data else None,
|
|
1059
1131
|
data["resolvedSearchType"] if "resolvedSearchType" in data else None,
|
|
1060
1132
|
data["autoDate"] if "autoDate" in data else None,
|
|
1133
|
+
cost_dollars=cost_dollars,
|
|
1061
1134
|
)
|
|
1062
1135
|
|
|
1063
1136
|
@overload
|
|
@@ -1071,8 +1144,7 @@ class Exa:
|
|
|
1071
1144
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1072
1145
|
extras: Optional[ExtrasOptions] = None,
|
|
1073
1146
|
flags: Optional[List[str]] = None,
|
|
1074
|
-
) -> SearchResponse[ResultWithText]:
|
|
1075
|
-
...
|
|
1147
|
+
) -> SearchResponse[ResultWithText]: ...
|
|
1076
1148
|
|
|
1077
1149
|
@overload
|
|
1078
1150
|
def get_contents(
|
|
@@ -1087,8 +1159,7 @@ class Exa:
|
|
|
1087
1159
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1088
1160
|
extras: Optional[ExtrasOptions] = None,
|
|
1089
1161
|
flags: Optional[List[str]] = None,
|
|
1090
|
-
) -> SearchResponse[ResultWithText]:
|
|
1091
|
-
...
|
|
1162
|
+
) -> SearchResponse[ResultWithText]: ...
|
|
1092
1163
|
|
|
1093
1164
|
@overload
|
|
1094
1165
|
def get_contents(
|
|
@@ -1103,8 +1174,7 @@ class Exa:
|
|
|
1103
1174
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1104
1175
|
extras: Optional[ExtrasOptions] = None,
|
|
1105
1176
|
flags: Optional[List[str]] = None,
|
|
1106
|
-
) -> SearchResponse[ResultWithHighlights]:
|
|
1107
|
-
...
|
|
1177
|
+
) -> SearchResponse[ResultWithHighlights]: ...
|
|
1108
1178
|
|
|
1109
1179
|
@overload
|
|
1110
1180
|
def get_contents(
|
|
@@ -1120,8 +1190,7 @@ class Exa:
|
|
|
1120
1190
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1121
1191
|
extras: Optional[ExtrasOptions] = None,
|
|
1122
1192
|
flags: Optional[List[str]] = None,
|
|
1123
|
-
) -> SearchResponse[ResultWithTextAndHighlights]:
|
|
1124
|
-
...
|
|
1193
|
+
) -> SearchResponse[ResultWithTextAndHighlights]: ...
|
|
1125
1194
|
|
|
1126
1195
|
@overload
|
|
1127
1196
|
def get_contents(
|
|
@@ -1136,8 +1205,7 @@ class Exa:
|
|
|
1136
1205
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1137
1206
|
extras: Optional[ExtrasOptions] = None,
|
|
1138
1207
|
flags: Optional[List[str]] = None,
|
|
1139
|
-
) -> SearchResponse[ResultWithSummary]:
|
|
1140
|
-
...
|
|
1208
|
+
) -> SearchResponse[ResultWithSummary]: ...
|
|
1141
1209
|
|
|
1142
1210
|
@overload
|
|
1143
1211
|
def get_contents(
|
|
@@ -1153,8 +1221,7 @@ class Exa:
|
|
|
1153
1221
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1154
1222
|
extras: Optional[ExtrasOptions] = None,
|
|
1155
1223
|
flags: Optional[List[str]] = None,
|
|
1156
|
-
) -> SearchResponse[ResultWithTextAndSummary]:
|
|
1157
|
-
...
|
|
1224
|
+
) -> SearchResponse[ResultWithTextAndSummary]: ...
|
|
1158
1225
|
|
|
1159
1226
|
@overload
|
|
1160
1227
|
def get_contents(
|
|
@@ -1170,8 +1237,7 @@ class Exa:
|
|
|
1170
1237
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1171
1238
|
extras: Optional[ExtrasOptions] = None,
|
|
1172
1239
|
flags: Optional[List[str]] = None,
|
|
1173
|
-
) -> SearchResponse[ResultWithHighlightsAndSummary]:
|
|
1174
|
-
...
|
|
1240
|
+
) -> SearchResponse[ResultWithHighlightsAndSummary]: ...
|
|
1175
1241
|
|
|
1176
1242
|
@overload
|
|
1177
1243
|
def get_contents(
|
|
@@ -1188,8 +1254,7 @@ class Exa:
|
|
|
1188
1254
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1189
1255
|
extras: Optional[ExtrasOptions] = None,
|
|
1190
1256
|
flags: Optional[List[str]] = None,
|
|
1191
|
-
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
|
|
1192
|
-
...
|
|
1257
|
+
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...
|
|
1193
1258
|
|
|
1194
1259
|
def get_contents(self, urls: Union[str, List[str], List[_Result]], **kwargs):
|
|
1195
1260
|
options = {
|
|
@@ -1211,11 +1276,13 @@ class Exa:
|
|
|
1211
1276
|
)
|
|
1212
1277
|
options = to_camel_case(options)
|
|
1213
1278
|
data = self.request("/contents", options)
|
|
1279
|
+
cost_dollars = parse_cost_dollars(data.get("costDollars"))
|
|
1214
1280
|
return SearchResponse(
|
|
1215
1281
|
[Result(**to_snake_case(result)) for result in data["results"]],
|
|
1216
1282
|
data.get("autopromptString"),
|
|
1217
1283
|
data.get("resolvedSearchType"),
|
|
1218
1284
|
data.get("autoDate"),
|
|
1285
|
+
cost_dollars=cost_dollars,
|
|
1219
1286
|
)
|
|
1220
1287
|
|
|
1221
1288
|
def find_similar(
|
|
@@ -1259,11 +1326,13 @@ class Exa:
|
|
|
1259
1326
|
validate_search_options(options, FIND_SIMILAR_OPTIONS_TYPES)
|
|
1260
1327
|
options = to_camel_case(options)
|
|
1261
1328
|
data = self.request("/findSimilar", options)
|
|
1329
|
+
cost_dollars = parse_cost_dollars(data.get("costDollars"))
|
|
1262
1330
|
return SearchResponse(
|
|
1263
1331
|
[Result(**to_snake_case(result)) for result in data["results"]],
|
|
1264
1332
|
data.get("autopromptString"),
|
|
1265
1333
|
data.get("resolvedSearchType"),
|
|
1266
1334
|
data.get("autoDate"),
|
|
1335
|
+
cost_dollars=cost_dollars,
|
|
1267
1336
|
)
|
|
1268
1337
|
|
|
1269
1338
|
@overload
|
|
@@ -1289,8 +1358,7 @@ class Exa:
|
|
|
1289
1358
|
subpages: Optional[int] = None,
|
|
1290
1359
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1291
1360
|
extras: Optional[ExtrasOptions] = None,
|
|
1292
|
-
) -> SearchResponse[ResultWithText]:
|
|
1293
|
-
...
|
|
1361
|
+
) -> SearchResponse[ResultWithText]: ...
|
|
1294
1362
|
|
|
1295
1363
|
@overload
|
|
1296
1364
|
def find_similar_and_contents(
|
|
@@ -1316,8 +1384,7 @@ class Exa:
|
|
|
1316
1384
|
subpages: Optional[int] = None,
|
|
1317
1385
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1318
1386
|
extras: Optional[ExtrasOptions] = None,
|
|
1319
|
-
) -> SearchResponse[ResultWithText]:
|
|
1320
|
-
...
|
|
1387
|
+
) -> SearchResponse[ResultWithText]: ...
|
|
1321
1388
|
|
|
1322
1389
|
@overload
|
|
1323
1390
|
def find_similar_and_contents(
|
|
@@ -1343,8 +1410,7 @@ class Exa:
|
|
|
1343
1410
|
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
|
|
1344
1411
|
filter_empty_results: Optional[bool] = None,
|
|
1345
1412
|
extras: Optional[ExtrasOptions] = None,
|
|
1346
|
-
) -> SearchResponse[ResultWithHighlights]:
|
|
1347
|
-
...
|
|
1413
|
+
) -> SearchResponse[ResultWithHighlights]: ...
|
|
1348
1414
|
|
|
1349
1415
|
@overload
|
|
1350
1416
|
def find_similar_and_contents(
|
|
@@ -1371,8 +1437,7 @@ class Exa:
|
|
|
1371
1437
|
subpages: Optional[int] = None,
|
|
1372
1438
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1373
1439
|
extras: Optional[ExtrasOptions] = None,
|
|
1374
|
-
) -> SearchResponse[ResultWithTextAndHighlights]:
|
|
1375
|
-
...
|
|
1440
|
+
) -> SearchResponse[ResultWithTextAndHighlights]: ...
|
|
1376
1441
|
|
|
1377
1442
|
@overload
|
|
1378
1443
|
def find_similar_and_contents(
|
|
@@ -1398,8 +1463,7 @@ class Exa:
|
|
|
1398
1463
|
subpages: Optional[int] = None,
|
|
1399
1464
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1400
1465
|
extras: Optional[ExtrasOptions] = None,
|
|
1401
|
-
) -> SearchResponse[ResultWithSummary]:
|
|
1402
|
-
...
|
|
1466
|
+
) -> SearchResponse[ResultWithSummary]: ...
|
|
1403
1467
|
|
|
1404
1468
|
@overload
|
|
1405
1469
|
def find_similar_and_contents(
|
|
@@ -1426,8 +1490,7 @@ class Exa:
|
|
|
1426
1490
|
subpages: Optional[int] = None,
|
|
1427
1491
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1428
1492
|
extras: Optional[ExtrasOptions] = None,
|
|
1429
|
-
) -> SearchResponse[ResultWithTextAndSummary]:
|
|
1430
|
-
...
|
|
1493
|
+
) -> SearchResponse[ResultWithTextAndSummary]: ...
|
|
1431
1494
|
|
|
1432
1495
|
@overload
|
|
1433
1496
|
def find_similar_and_contents(
|
|
@@ -1454,8 +1517,7 @@ class Exa:
|
|
|
1454
1517
|
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
|
|
1455
1518
|
filter_empty_results: Optional[bool] = None,
|
|
1456
1519
|
extras: Optional[ExtrasOptions] = None,
|
|
1457
|
-
) -> SearchResponse[ResultWithHighlightsAndSummary]:
|
|
1458
|
-
...
|
|
1520
|
+
) -> SearchResponse[ResultWithHighlightsAndSummary]: ...
|
|
1459
1521
|
|
|
1460
1522
|
@overload
|
|
1461
1523
|
def find_similar_and_contents(
|
|
@@ -1483,8 +1545,7 @@ class Exa:
|
|
|
1483
1545
|
subpages: Optional[int] = None,
|
|
1484
1546
|
subpage_target: Optional[Union[str, List[str]]] = None,
|
|
1485
1547
|
extras: Optional[ExtrasOptions] = None,
|
|
1486
|
-
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
|
|
1487
|
-
...
|
|
1548
|
+
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...
|
|
1488
1549
|
|
|
1489
1550
|
def find_similar_and_contents(self, url: str, **kwargs):
|
|
1490
1551
|
options = {k: v for k, v in {"url": url, **kwargs}.items() if v is not None}
|
|
@@ -1521,18 +1582,20 @@ class Exa:
|
|
|
1521
1582
|
)
|
|
1522
1583
|
options = to_camel_case(options)
|
|
1523
1584
|
data = self.request("/findSimilar", options)
|
|
1585
|
+
cost_dollars = parse_cost_dollars(data.get("costDollars"))
|
|
1524
1586
|
return SearchResponse(
|
|
1525
1587
|
[Result(**to_snake_case(result)) for result in data["results"]],
|
|
1526
1588
|
data.get("autopromptString"),
|
|
1527
1589
|
data.get("resolvedSearchType"),
|
|
1528
1590
|
data.get("autoDate"),
|
|
1591
|
+
cost_dollars=cost_dollars,
|
|
1529
1592
|
)
|
|
1530
1593
|
|
|
1531
1594
|
def wrap(self, client: OpenAI):
|
|
1532
1595
|
"""Wrap an OpenAI client with Exa functionality.
|
|
1533
1596
|
|
|
1534
|
-
After wrapping, any call to `client.chat.completions.create` will be intercepted
|
|
1535
|
-
and enhanced with Exa RAG functionality. To disable Exa for a specific call,
|
|
1597
|
+
After wrapping, any call to `client.chat.completions.create` will be intercepted
|
|
1598
|
+
and enhanced with Exa RAG functionality. To disable Exa for a specific call,
|
|
1536
1599
|
set `use_exa="none"` in the `create` method.
|
|
1537
1600
|
|
|
1538
1601
|
Args:
|
|
@@ -1662,8 +1725,7 @@ class Exa:
|
|
|
1662
1725
|
stream: Optional[bool] = False,
|
|
1663
1726
|
text: Optional[bool] = False,
|
|
1664
1727
|
model: Optional[Literal["exa", "exa-pro"]] = None,
|
|
1665
|
-
) -> Union[AnswerResponse, StreamAnswerResponse]:
|
|
1666
|
-
...
|
|
1728
|
+
) -> Union[AnswerResponse, StreamAnswerResponse]: ...
|
|
1667
1729
|
|
|
1668
1730
|
def answer(
|
|
1669
1731
|
self,
|
|
@@ -1692,17 +1754,13 @@ class Exa:
|
|
|
1692
1754
|
"Please use `stream_answer(...)` for streaming."
|
|
1693
1755
|
)
|
|
1694
1756
|
|
|
1695
|
-
options = {
|
|
1696
|
-
k: v
|
|
1697
|
-
for k, v in locals().items()
|
|
1698
|
-
if k != "self" and v is not None
|
|
1699
|
-
}
|
|
1757
|
+
options = {k: v for k, v in locals().items() if k != "self" and v is not None}
|
|
1700
1758
|
options = to_camel_case(options)
|
|
1701
1759
|
response = self.request("/answer", options)
|
|
1702
1760
|
|
|
1703
1761
|
return AnswerResponse(
|
|
1704
1762
|
response["answer"],
|
|
1705
|
-
[AnswerResult(**to_snake_case(result)) for result in response["citations"]]
|
|
1763
|
+
[AnswerResult(**to_snake_case(result)) for result in response["citations"]],
|
|
1706
1764
|
)
|
|
1707
1765
|
|
|
1708
1766
|
def stream_answer(
|
|
@@ -1723,13 +1781,8 @@ class Exa:
|
|
|
1723
1781
|
StreamAnswerResponse: An object that can be iterated over to retrieve (partial text, partial citations).
|
|
1724
1782
|
Each iteration yields a tuple of (Optional[str], Optional[List[AnswerResult]]).
|
|
1725
1783
|
"""
|
|
1726
|
-
options = {
|
|
1727
|
-
k: v
|
|
1728
|
-
for k, v in locals().items()
|
|
1729
|
-
if k != "self" and v is not None
|
|
1730
|
-
}
|
|
1784
|
+
options = {k: v for k, v in locals().items() if k != "self" and v is not None}
|
|
1731
1785
|
options = to_camel_case(options)
|
|
1732
1786
|
options["stream"] = True
|
|
1733
1787
|
raw_response = self.request("/answer", options)
|
|
1734
1788
|
return StreamAnswerResponse(raw_response)
|
|
1735
|
-
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: exa_py
|
|
3
|
+
Version: 1.8.9
|
|
4
|
+
Summary: Python SDK for Exa API.
|
|
5
|
+
Home-page: https://github.com/exa-labs/exa-py
|
|
6
|
+
Author: Exa
|
|
7
|
+
Author-email: hello@exa.ai
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Requires-Dist: typing-extensions
|
|
20
|
+
Requires-Dist: openai>=1.10.0
|
|
21
|
+
|
|
22
|
+
# Exa
|
|
23
|
+
|
|
24
|
+
Exa (formerly Metaphor) API in Python
|
|
25
|
+
|
|
26
|
+
Note: This API is basically the same as `metaphor-python` but reflects new
|
|
27
|
+
features associated with Metaphor's rename to Exa. New site is https://exa.ai
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install exa_py
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Import the package and initialize the Exa client with your API key:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from exa_py import Exa
|
|
41
|
+
|
|
42
|
+
exa = Exa(api_key="your-api-key")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Common requests
|
|
46
|
+
```python
|
|
47
|
+
|
|
48
|
+
# basic search
|
|
49
|
+
results = exa.search("This is a Exa query:")
|
|
50
|
+
|
|
51
|
+
# autoprompted search
|
|
52
|
+
results = exa.search("autopromptable query", use_autoprompt=True)
|
|
53
|
+
|
|
54
|
+
# keyword search (non-neural)
|
|
55
|
+
results = exa.search("Google-style query", type="keyword")
|
|
56
|
+
|
|
57
|
+
# search with date filters
|
|
58
|
+
results = exa.search("This is a Exa query:", start_published_date="2019-01-01", end_published_date="2019-01-31")
|
|
59
|
+
|
|
60
|
+
# search with domain filters
|
|
61
|
+
results = exa.search("This is a Exa query:", include_domains=["www.cnn.com", "www.nytimes.com"])
|
|
62
|
+
|
|
63
|
+
# search and get text contents
|
|
64
|
+
results = exa.search_and_contents("This is a Exa query:")
|
|
65
|
+
|
|
66
|
+
# search and get highlights
|
|
67
|
+
results = exa.search_and_contents("This is a Exa query:", highlights=True)
|
|
68
|
+
|
|
69
|
+
# search and get contents with contents options
|
|
70
|
+
results = exa.search_and_contents("This is a Exa query:",
|
|
71
|
+
text={"include_html_tags": True, "max_characters": 1000},
|
|
72
|
+
highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
|
|
73
|
+
|
|
74
|
+
# find similar documents
|
|
75
|
+
results = exa.find_similar("https://example.com")
|
|
76
|
+
|
|
77
|
+
# find similar excluding source domain
|
|
78
|
+
results = exa.find_similar("https://example.com", exclude_source_domain=True)
|
|
79
|
+
|
|
80
|
+
# find similar with contents
|
|
81
|
+
results = exa.find_similar_and_contents("https://example.com", text=True, highlights=True)
|
|
82
|
+
|
|
83
|
+
# get text contents
|
|
84
|
+
results = exa.get_contents(["urls"])
|
|
85
|
+
|
|
86
|
+
# get highlights
|
|
87
|
+
results = exa.get_contents(["urls"], highlights=True)
|
|
88
|
+
|
|
89
|
+
# get contents with contents options
|
|
90
|
+
results = exa.get_contents(["urls"],
|
|
91
|
+
text={"include_html_tags": True, "max_characters": 1000},
|
|
92
|
+
highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
|
|
93
|
+
|
|
94
|
+
# basic answer
|
|
95
|
+
response = exa.answer("This is a query to answer a question")
|
|
96
|
+
|
|
97
|
+
# answer with full text, using the exa-pro model (sends 2 expanded quries to exa search)
|
|
98
|
+
response = exa.answer("This is a query to answer a question", text=True, model="exa-pro")
|
|
99
|
+
|
|
100
|
+
# answer with streaming
|
|
101
|
+
response = exa.stream_answer("This is a query to answer:")
|
|
102
|
+
|
|
103
|
+
# Print each chunk as it arrives when using the stream_answer method
|
|
104
|
+
for chunk in response:
|
|
105
|
+
print(chunk, end='', flush=True)
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
exa_py/__init__.py
|
|
5
|
+
exa_py/api.py
|
|
6
|
+
exa_py/py.typed
|
|
7
|
+
exa_py/utils.py
|
|
8
|
+
exa_py.egg-info/PKG-INFO
|
|
9
|
+
exa_py.egg-info/SOURCES.txt
|
|
10
|
+
exa_py.egg-info/dependency_links.txt
|
|
11
|
+
exa_py.egg-info/requires.txt
|
|
12
|
+
exa_py.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exa_py
|
exa_py-1.8.9/setup.cfg
ADDED
exa_py-1.8.9/setup.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="exa_py",
|
|
5
|
+
version="1.8.9",
|
|
6
|
+
description="Python SDK for Exa API.",
|
|
7
|
+
long_description_content_type="text/markdown",
|
|
8
|
+
long_description=open("README.md").read(),
|
|
9
|
+
author="Exa",
|
|
10
|
+
author_email="hello@exa.ai",
|
|
11
|
+
package_data={"exa_py": ["py.typed"]},
|
|
12
|
+
url="https://github.com/exa-labs/exa-py",
|
|
13
|
+
packages=find_packages(),
|
|
14
|
+
install_requires=["requests", "typing-extensions", "openai>=1.10.0"],
|
|
15
|
+
classifiers=[
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
],
|
|
26
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|