exa-py 1.7.3__tar.gz → 1.8.4__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.4/PKG-INFO ADDED
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.1
2
+ Name: exa_py
3
+ Version: 1.8.4
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
+
19
+ # Exa
20
+
21
+ Exa (formerly Metaphor) API in Python
22
+
23
+ Note: This API is basically the same as `metaphor-python` but reflects new
24
+ features associated with Metaphor's rename to Exa. New site is https://exa.ai
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install exa_py
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Import the package and initialize the Exa client with your API key:
35
+
36
+ ```python
37
+ from exa_py import Exa
38
+
39
+ exa = Exa(api_key="your-api-key")
40
+ ```
41
+
42
+ ## Common requests
43
+ ```python
44
+
45
+ # basic search
46
+ results = exa.search("This is a Exa query:")
47
+
48
+ # autoprompted search
49
+ results = exa.search("autopromptable query", use_autoprompt=True)
50
+
51
+ # keyword search (non-neural)
52
+ results = exa.search("Google-style query", type="keyword")
53
+
54
+ # search with date filters
55
+ results = exa.search("This is a Exa query:", start_published_date="2019-01-01", end_published_date="2019-01-31")
56
+
57
+ # search with domain filters
58
+ results = exa.search("This is a Exa query:", include_domains=["www.cnn.com", "www.nytimes.com"])
59
+
60
+ # search and get text contents
61
+ results = exa.search_and_contents("This is a Exa query:")
62
+
63
+ # search and get highlights
64
+ results = exa.search_and_contents("This is a Exa query:", highlights=True)
65
+
66
+ # search and get contents with contents options
67
+ results = exa.search_and_contents("This is a Exa query:",
68
+ text={"include_html_tags": True, "max_characters": 1000},
69
+ highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
70
+
71
+ # find similar documents
72
+ results = exa.find_similar("https://example.com")
73
+
74
+ # find similar excluding source domain
75
+ results = exa.find_similar("https://example.com", exclude_source_domain=True)
76
+
77
+ # find similar with contents
78
+ results = exa.find_similar_and_contents("https://example.com", text=True, highlights=True)
79
+
80
+ # get text contents
81
+ results = exa.get_contents(["urls"])
82
+
83
+ # get highlights
84
+ results = exa.get_contents(["urls"], highlights=True)
85
+
86
+ # get contents with contents options
87
+ results = exa.get_contents(["urls"],
88
+ text={"include_html_tags": True, "max_characters": 1000},
89
+ highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
90
+
91
+ # basic answer
92
+ response = exa.answer("This is a query to answer a question")
93
+
94
+ # answer with expanded queries and full text
95
+ response = exa.answer("This is a query to answer a question", expanded_queries_limit=3, include_text=True)
96
+
97
+ # answer with streaming
98
+ response = exa.answer("This is a query to answer with streaming:", stream=True)
99
+
100
+ # Print each chunk as it arrives when answer streaming is enabled
101
+ for chunk in response:
102
+ print(chunk)
103
+ ```
104
+
@@ -69,5 +69,18 @@ exa = Exa(api_key="your-api-key")
69
69
  results = exa.get_contents(["urls"],
70
70
  text={"include_html_tags": True, "max_characters": 1000},
71
71
  highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
72
+
73
+ # basic answer
74
+ response = exa.answer("This is a query to answer a question")
75
+
76
+ # answer with expanded queries and full text
77
+ response = exa.answer("This is a query to answer a question", expanded_queries_limit=3, include_text=True)
78
+
79
+ # answer with streaming
80
+ response = exa.answer("This is a query to answer with streaming:", stream=True)
81
+
82
+ # Print each chunk as it arrives when answer streaming is enabled
83
+ for chunk in response:
84
+ print(chunk)
72
85
  ```
73
86
 
@@ -30,6 +30,7 @@ from exa_py.utils import (
30
30
  maybe_get_query,
31
31
  )
32
32
  import os
33
+ from typing import Iterator
33
34
 
34
35
  is_beta = os.getenv("IS_BETA") == "True"
35
36
 
@@ -57,11 +58,13 @@ def to_camel_case(data: dict) -> dict:
57
58
  Returns:
58
59
  dict: The dictionary with keys converted to camelCase format.
59
60
  """
60
- return {
61
- snake_to_camel(k): to_camel_case(v) if isinstance(v, dict) else v
62
- for k, v in data.items()
63
- if v is not None
64
- }
61
+ if isinstance(data, dict):
62
+ return {
63
+ snake_to_camel(k): to_camel_case(v) if isinstance(v, dict) else v
64
+ for k, v in data.items()
65
+ if v is not None
66
+ }
67
+ return data
65
68
 
66
69
 
67
70
  def camel_to_snake(camel_str: str) -> str:
@@ -87,10 +90,12 @@ def to_snake_case(data: dict) -> dict:
87
90
  Returns:
88
91
  dict: The dictionary with keys converted to snake_case format.
89
92
  """
90
- return {
91
- camel_to_snake(k): to_snake_case(v) if isinstance(v, dict) else v
92
- for k, v in data.items()
93
- }
93
+ if isinstance(data, dict):
94
+ return {
95
+ camel_to_snake(k): to_snake_case(v) if isinstance(v, dict) else v
96
+ for k, v in data.items()
97
+ }
98
+ return data
94
99
 
95
100
 
96
101
  SEARCH_OPTIONS_TYPES = {
@@ -105,9 +110,10 @@ SEARCH_OPTIONS_TYPES = {
105
110
  "include_text": [list], # Must be present in webpage text. (One string, up to 5 words)
106
111
  "exclude_text": [list], # Must not be present in webpage text. (One string, up to 5 words)
107
112
  "use_autoprompt": [bool], # Convert query to Exa. (Default: false)
108
- "type": [str], # 'keyword', 'neural', or 'auto' (Default: auto).'neural' uses embeddings search, 'keyword' is SERP and 'auto' decides the best search type based on your query
113
+ "type": [str], # 'keyword', 'neural', or 'auto' (Default: auto)
109
114
  "category": [str], # A data category to focus on: 'company', 'research paper', 'news', 'pdf', 'github', 'tweet', 'personal site', 'linkedin profile', 'financial report'
110
- "flags": [list], # Experimental flags array for Exa usage.
115
+ "flags": [list], # Experimental flags array for Exa usage.
116
+ "moderation": [bool], # If true, moderate search results for safety.
111
117
  }
112
118
 
113
119
  FIND_SIMILAR_OPTIONS_TYPES = {
@@ -123,7 +129,7 @@ FIND_SIMILAR_OPTIONS_TYPES = {
123
129
  "exclude_text": [list],
124
130
  "exclude_source_domain": [bool],
125
131
  "category": [str],
126
- "flags": [list], # Experimental flags array for Exa usage.
132
+ "flags": [list], # Experimental flags array for Exa usage.
127
133
  }
128
134
 
129
135
  # the livecrawl options
@@ -197,7 +203,7 @@ class HighlightsContentsOptions(TypedDict, total=False):
197
203
  """A class representing the options that you can specify when requesting highlights
198
204
 
199
205
  Attributes:
200
- query (str): The query string for the highlights.
206
+ query (str): The query string for the highlights.
201
207
  num_sentences (int): Size of highlights to return, in sentences. Default: 5
202
208
  highlights_per_url (int): Number of highlights to return per URL. Default: 1
203
209
  """
@@ -216,6 +222,7 @@ class SummaryContentsOptions(TypedDict, total=False):
216
222
 
217
223
  query: str
218
224
 
225
+
219
226
  class ExtrasOptions(TypedDict, total=False):
220
227
  """A class representing additional extraction fields (e.g. links, images)"""
221
228
 
@@ -495,6 +502,59 @@ class ResultWithTextAndHighlightsAndSummary(_Result):
495
502
  )
496
503
 
497
504
 
505
+ @dataclass
506
+ class AnswerResult:
507
+ """A class representing a source result for an answer.
508
+
509
+ Attributes:
510
+ title (str): The title of the search result.
511
+ url (str): The URL of the search result.
512
+ id (str): The temporary ID for the document.
513
+ published_date (str, optional): An estimate of the creation date, from parsing HTML content.
514
+ author (str, optional): If available, the author of the content.
515
+ """
516
+
517
+ url: str
518
+ id: str
519
+ title: Optional[str] = None
520
+ published_date: Optional[str] = None
521
+ author: Optional[str] = None
522
+
523
+ def __init__(self, **kwargs):
524
+ self.url = kwargs["url"]
525
+ self.id = kwargs["id"]
526
+ self.title = kwargs.get("title")
527
+ self.published_date = kwargs.get("published_date")
528
+ self.author = kwargs.get("author")
529
+
530
+ def __str__(self):
531
+ return (
532
+ f"Title: {self.title}\n"
533
+ f"URL: {self.url}\n"
534
+ f"ID: {self.id}\n"
535
+ f"Published Date: {self.published_date}\n"
536
+ f"Author: {self.author}\n"
537
+ )
538
+
539
+
540
+ @dataclass
541
+ class AnswerResponse:
542
+ """A class representing the response for an answer operation.
543
+
544
+ Attributes:
545
+ answer (str): The generated answer.
546
+ sources (List[AnswerResult]): A list of sources used to generate the answer.
547
+ """
548
+
549
+ answer: str
550
+ sources: List[AnswerResult]
551
+
552
+ def __str__(self):
553
+ output = f"Answer: {self.answer}\n\nSources:\n"
554
+ output += "\n\n".join(str(source) for source in self.sources)
555
+ return output
556
+
557
+
498
558
  T = TypeVar("T")
499
559
 
500
560
 
@@ -548,7 +608,7 @@ class Exa:
548
608
  self,
549
609
  api_key: Optional[str],
550
610
  base_url: str = "https://api.exa.ai",
551
- user_agent: str = "exa-py 1.7.3",
611
+ user_agent: str = "exa-py 1.8.4",
552
612
  ):
553
613
  """Initialize the Exa client with the provided API key and optional base URL and user agent.
554
614
 
@@ -568,11 +628,28 @@ class Exa:
568
628
  self.headers = {"x-api-key": api_key, "User-Agent": user_agent}
569
629
 
570
630
  def request(self, endpoint: str, data):
631
+ """Send a POST request to the Exa API, optionally streaming if data['stream'] is True.
632
+
633
+ Args:
634
+ endpoint (str): The API endpoint (path).
635
+ data (dict): The JSON payload to send.
636
+
637
+ Returns:
638
+ Union[dict, Iterator[str]]: If streaming, returns an iterator of strings (line-by-line).
639
+ Otherwise, returns the JSON-decoded response as a dict.
640
+
641
+ Raises:
642
+ ValueError: If the request fails (non-200 status code).
643
+ """
644
+ if data.get("stream"):
645
+ res = requests.post(self.base_url + endpoint, json=data, headers=self.headers, stream=True)
646
+ if res.status_code != 200:
647
+ raise ValueError(f"Request failed with status code {res.status_code}: {res.text}")
648
+ return (line.decode("utf-8") for line in res.iter_lines() if line)
649
+
571
650
  res = requests.post(self.base_url + endpoint, json=data, headers=self.headers)
572
651
  if res.status_code != 200:
573
- raise ValueError(
574
- f"Request failed with status code {res.status_code}: {res.text}"
575
- )
652
+ raise ValueError(f"Request failed with status code {res.status_code}: {res.text}")
576
653
  return res.json()
577
654
 
578
655
  def search(
@@ -592,6 +669,7 @@ class Exa:
592
669
  type: Optional[str] = None,
593
670
  category: Optional[str] = None,
594
671
  flags: Optional[List[str]] = None,
672
+ moderation: Optional[bool] = None,
595
673
  ) -> SearchResponse[_Result]:
596
674
  """Perform a search with a prompt-engineered query to retrieve relevant results.
597
675
 
@@ -610,6 +688,7 @@ class Exa:
610
688
  type (str, optional): 'keyword' or 'neural' (default 'neural').
611
689
  category (str, optional): e.g. 'company'
612
690
  flags (List[str], optional): Experimental flags for Exa usage.
691
+ moderation (bool, optional): If True, the search results will be moderated for safety.
613
692
 
614
693
  Returns:
615
694
  SearchResponse: The response containing search results, etc.
@@ -642,12 +721,13 @@ class Exa:
642
721
  use_autoprompt: Optional[bool] = None,
643
722
  type: Optional[str] = None,
644
723
  category: Optional[str] = None,
724
+ flags: Optional[List[str]] = None,
725
+ moderation: Optional[bool] = None,
645
726
  livecrawl_timeout: Optional[int] = None,
646
727
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
647
728
  filter_empty_results: Optional[bool] = None,
648
729
  subpages: Optional[int] = None,
649
730
  extras: Optional[ExtrasOptions] = None,
650
- flags: Optional[List[str]] = None,
651
731
  ) -> SearchResponse[ResultWithText]:
652
732
  ...
653
733
 
@@ -669,12 +749,13 @@ class Exa:
669
749
  use_autoprompt: Optional[bool] = None,
670
750
  type: Optional[str] = None,
671
751
  category: Optional[str] = None,
752
+ flags: Optional[List[str]] = None,
753
+ moderation: Optional[bool] = None,
672
754
  subpages: Optional[int] = None,
673
755
  livecrawl_timeout: Optional[int] = None,
674
756
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
675
757
  filter_empty_results: Optional[bool] = None,
676
758
  extras: Optional[ExtrasOptions] = None,
677
- flags: Optional[List[str]] = None,
678
759
  ) -> SearchResponse[ResultWithText]:
679
760
  ...
680
761
 
@@ -698,11 +779,12 @@ class Exa:
698
779
  category: Optional[str] = None,
699
780
  subpages: Optional[int] = None,
700
781
  subpage_target: Optional[Union[str, List[str]]] = None,
782
+ flags: Optional[List[str]] = None,
783
+ moderation: Optional[bool] = None,
701
784
  livecrawl_timeout: Optional[int] = None,
702
785
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
703
786
  filter_empty_results: Optional[bool] = None,
704
787
  extras: Optional[ExtrasOptions] = None,
705
- flags: Optional[List[str]] = None,
706
788
  ) -> SearchResponse[ResultWithHighlights]:
707
789
  ...
708
790
 
@@ -725,13 +807,14 @@ class Exa:
725
807
  use_autoprompt: Optional[bool] = None,
726
808
  type: Optional[str] = None,
727
809
  category: Optional[str] = None,
728
- livecrawl_timeout: Optional[int] = None,
729
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
730
810
  subpages: Optional[int] = None,
731
811
  subpage_target: Optional[Union[str, List[str]]] = None,
812
+ flags: Optional[List[str]] = None,
813
+ moderation: Optional[bool] = None,
814
+ livecrawl_timeout: Optional[int] = None,
815
+ livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
732
816
  filter_empty_results: Optional[bool] = None,
733
817
  extras: Optional[ExtrasOptions] = None,
734
- flags: Optional[List[str]] = None,
735
818
  ) -> SearchResponse[ResultWithTextAndHighlights]:
736
819
  ...
737
820
 
@@ -755,11 +838,12 @@ class Exa:
755
838
  category: Optional[str] = None,
756
839
  subpages: Optional[int] = None,
757
840
  subpage_target: Optional[Union[str, List[str]]] = None,
841
+ flags: Optional[List[str]] = None,
842
+ moderation: Optional[bool] = None,
758
843
  livecrawl_timeout: Optional[int] = None,
759
844
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
760
845
  filter_empty_results: Optional[bool] = None,
761
846
  extras: Optional[ExtrasOptions] = None,
762
- flags: Optional[List[str]] = None,
763
847
  ) -> SearchResponse[ResultWithSummary]:
764
848
  ...
765
849
 
@@ -784,11 +868,12 @@ class Exa:
784
868
  category: Optional[str] = None,
785
869
  subpages: Optional[int] = None,
786
870
  subpage_target: Optional[Union[str, List[str]]] = None,
871
+ flags: Optional[List[str]] = None,
872
+ moderation: Optional[bool] = None,
787
873
  livecrawl_timeout: Optional[int] = None,
788
874
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
789
875
  filter_empty_results: Optional[bool] = None,
790
876
  extras: Optional[ExtrasOptions] = None,
791
- flags: Optional[List[str]] = None,
792
877
  ) -> SearchResponse[ResultWithTextAndSummary]:
793
878
  ...
794
879
 
@@ -813,11 +898,12 @@ class Exa:
813
898
  category: Optional[str] = None,
814
899
  subpages: Optional[int] = None,
815
900
  subpage_target: Optional[Union[str, List[str]]] = None,
901
+ flags: Optional[List[str]] = None,
902
+ moderation: Optional[bool] = None,
816
903
  livecrawl_timeout: Optional[int] = None,
817
904
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
818
905
  filter_empty_results: Optional[bool] = None,
819
906
  extras: Optional[ExtrasOptions] = None,
820
- flags: Optional[List[str]] = None,
821
907
  ) -> SearchResponse[ResultWithHighlightsAndSummary]:
822
908
  ...
823
909
 
@@ -841,13 +927,14 @@ class Exa:
841
927
  use_autoprompt: Optional[bool] = None,
842
928
  type: Optional[str] = None,
843
929
  category: Optional[str] = None,
930
+ flags: Optional[List[str]] = None,
931
+ moderation: Optional[bool] = None,
844
932
  livecrawl_timeout: Optional[int] = None,
845
933
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
846
934
  subpages: Optional[int] = None,
847
935
  subpage_target: Optional[Union[str, List[str]]] = None,
848
936
  filter_empty_results: Optional[bool] = None,
849
937
  extras: Optional[ExtrasOptions] = None,
850
- flags: Optional[List[str]] = None,
851
938
  ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
852
939
  ...
853
940
 
@@ -864,7 +951,11 @@ class Exa:
864
951
 
865
952
  validate_search_options(
866
953
  options,
867
- {**SEARCH_OPTIONS_TYPES, **CONTENTS_OPTIONS_TYPES, **CONTENTS_ENDPOINT_OPTIONS_TYPES},
954
+ {
955
+ **SEARCH_OPTIONS_TYPES,
956
+ **CONTENTS_OPTIONS_TYPES,
957
+ **CONTENTS_ENDPOINT_OPTIONS_TYPES,
958
+ },
868
959
  )
869
960
 
870
961
  # Nest the appropriate fields under "contents"
@@ -1021,13 +1112,19 @@ class Exa:
1021
1112
  flags: Optional[List[str]] = None,
1022
1113
  ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
1023
1114
  ...
1115
+
1024
1116
  def get_contents(self, urls: Union[str, List[str], List[_Result]], **kwargs):
1025
1117
  options = {
1026
1118
  k: v
1027
1119
  for k, v in {"urls": urls, **kwargs}.items()
1028
1120
  if k != "self" and v is not None
1029
1121
  }
1030
- if "text" not in options and "highlights" not in options and "summary" not in options and "extras" not in options:
1122
+ if (
1123
+ "text" not in options
1124
+ and "highlights" not in options
1125
+ and "summary" not in options
1126
+ and "extras" not in options
1127
+ ):
1031
1128
  options["text"] = True
1032
1129
 
1033
1130
  validate_search_options(
@@ -1038,9 +1135,9 @@ class Exa:
1038
1135
  data = self.request("/contents", options)
1039
1136
  return SearchResponse(
1040
1137
  [Result(**to_snake_case(result)) for result in data["results"]],
1041
- data["autopromptString"] if "autopromptString" in data else None,
1042
- data["resolvedSearchType"] if "resolvedSearchType" in data else None,
1043
- data["autoDate"] if "autoDate" in data else None,
1138
+ data.get("autopromptString"),
1139
+ data.get("resolvedSearchType"),
1140
+ data.get("autoDate"),
1044
1141
  )
1045
1142
 
1046
1143
  def find_similar(
@@ -1060,15 +1157,35 @@ class Exa:
1060
1157
  category: Optional[str] = None,
1061
1158
  flags: Optional[List[str]] = None,
1062
1159
  ) -> SearchResponse[_Result]:
1160
+ """Finds similar pages to a given URL, potentially with domain filters and date filters.
1161
+
1162
+ Args:
1163
+ url (str): The URL to find similar pages for.
1164
+ num_results (int, optional): Number of results to return. Default is None (server default).
1165
+ include_domains (List[str], optional): Domains to include in the search.
1166
+ exclude_domains (List[str], optional): Domains to exclude from the search.
1167
+ start_crawl_date (str, optional): Only links crawled after this date.
1168
+ end_crawl_date (str, optional): Only links crawled before this date.
1169
+ start_published_date (str, optional): Only links published after this date.
1170
+ end_published_date (str, optional): Only links published before this date.
1171
+ include_text (List[str], optional): Strings that must appear in the page text.
1172
+ exclude_text (List[str], optional): Strings that must not appear in the page text.
1173
+ exclude_source_domain (bool, optional): Whether to exclude the source domain.
1174
+ category (str, optional): A data category to focus on.
1175
+ flags (List[str], optional): Experimental flags.
1176
+
1177
+ Returns:
1178
+ SearchResponse[_Result]
1179
+ """
1063
1180
  options = {k: v for k, v in locals().items() if k != "self" and v is not None}
1064
1181
  validate_search_options(options, FIND_SIMILAR_OPTIONS_TYPES)
1065
1182
  options = to_camel_case(options)
1066
1183
  data = self.request("/findSimilar", options)
1067
1184
  return SearchResponse(
1068
1185
  [Result(**to_snake_case(result)) for result in data["results"]],
1069
- data["autopromptString"] if "autopromptString" in data else None,
1070
- data["resolvedSearchType"] if "resolvedSearchType" in data else None,
1071
- data["autoDate"] if "autoDate" in data else None,
1186
+ data.get("autopromptString"),
1187
+ data.get("resolvedSearchType"),
1188
+ data.get("autoDate"),
1072
1189
  )
1073
1190
 
1074
1191
  @overload
@@ -1087,13 +1204,13 @@ class Exa:
1087
1204
  exclude_text: Optional[List[str]] = None,
1088
1205
  exclude_source_domain: Optional[bool] = None,
1089
1206
  category: Optional[str] = None,
1207
+ flags: Optional[List[str]] = None,
1090
1208
  livecrawl_timeout: Optional[int] = None,
1091
1209
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1210
+ filter_empty_results: Optional[bool] = None,
1092
1211
  subpages: Optional[int] = None,
1093
1212
  subpage_target: Optional[Union[str, List[str]]] = None,
1094
- filter_empty_results: Optional[bool] = None,
1095
1213
  extras: Optional[ExtrasOptions] = None,
1096
- flags: Optional[List[str]] = None,
1097
1214
  ) -> SearchResponse[ResultWithText]:
1098
1215
  ...
1099
1216
 
@@ -1114,13 +1231,13 @@ class Exa:
1114
1231
  exclude_text: Optional[List[str]] = None,
1115
1232
  exclude_source_domain: Optional[bool] = None,
1116
1233
  category: Optional[str] = None,
1234
+ flags: Optional[List[str]] = None,
1117
1235
  livecrawl_timeout: Optional[int] = None,
1118
1236
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1237
+ filter_empty_results: Optional[bool] = None,
1119
1238
  subpages: Optional[int] = None,
1120
1239
  subpage_target: Optional[Union[str, List[str]]] = None,
1121
- filter_empty_results: Optional[bool] = None,
1122
1240
  extras: Optional[ExtrasOptions] = None,
1123
- flags: Optional[List[str]] = None,
1124
1241
  ) -> SearchResponse[ResultWithText]:
1125
1242
  ...
1126
1243
 
@@ -1141,13 +1258,13 @@ class Exa:
1141
1258
  exclude_text: Optional[List[str]] = None,
1142
1259
  exclude_source_domain: Optional[bool] = None,
1143
1260
  category: Optional[str] = None,
1261
+ flags: Optional[List[str]] = None,
1144
1262
  subpages: Optional[int] = None,
1145
1263
  subpage_target: Optional[Union[str, List[str]]] = None,
1146
1264
  livecrawl_timeout: Optional[int] = None,
1147
1265
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1148
1266
  filter_empty_results: Optional[bool] = None,
1149
1267
  extras: Optional[ExtrasOptions] = None,
1150
- flags: Optional[List[str]] = None,
1151
1268
  ) -> SearchResponse[ResultWithHighlights]:
1152
1269
  ...
1153
1270
 
@@ -1169,13 +1286,13 @@ class Exa:
1169
1286
  exclude_text: Optional[List[str]] = None,
1170
1287
  exclude_source_domain: Optional[bool] = None,
1171
1288
  category: Optional[str] = None,
1289
+ flags: Optional[List[str]] = None,
1172
1290
  livecrawl_timeout: Optional[int] = None,
1173
1291
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1292
+ filter_empty_results: Optional[bool] = None,
1174
1293
  subpages: Optional[int] = None,
1175
1294
  subpage_target: Optional[Union[str, List[str]]] = None,
1176
- filter_empty_results: Optional[bool] = None,
1177
1295
  extras: Optional[ExtrasOptions] = None,
1178
- flags: Optional[List[str]] = None,
1179
1296
  ) -> SearchResponse[ResultWithTextAndHighlights]:
1180
1297
  ...
1181
1298
 
@@ -1196,13 +1313,13 @@ class Exa:
1196
1313
  exclude_text: Optional[List[str]] = None,
1197
1314
  exclude_source_domain: Optional[bool] = None,
1198
1315
  category: Optional[str] = None,
1316
+ flags: Optional[List[str]] = None,
1199
1317
  livecrawl_timeout: Optional[int] = None,
1200
1318
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1319
+ filter_empty_results: Optional[bool] = None,
1201
1320
  subpages: Optional[int] = None,
1202
1321
  subpage_target: Optional[Union[str, List[str]]] = None,
1203
- filter_empty_results: Optional[bool] = None,
1204
1322
  extras: Optional[ExtrasOptions] = None,
1205
- flags: Optional[List[str]] = None,
1206
1323
  ) -> SearchResponse[ResultWithSummary]:
1207
1324
  ...
1208
1325
 
@@ -1224,13 +1341,13 @@ class Exa:
1224
1341
  exclude_text: Optional[List[str]] = None,
1225
1342
  exclude_source_domain: Optional[bool] = None,
1226
1343
  category: Optional[str] = None,
1344
+ flags: Optional[List[str]] = None,
1227
1345
  livecrawl_timeout: Optional[int] = None,
1228
1346
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1347
+ filter_empty_results: Optional[bool] = None,
1229
1348
  subpages: Optional[int] = None,
1230
1349
  subpage_target: Optional[Union[str, List[str]]] = None,
1231
- filter_empty_results: Optional[bool] = None,
1232
1350
  extras: Optional[ExtrasOptions] = None,
1233
- flags: Optional[List[str]] = None,
1234
1351
  ) -> SearchResponse[ResultWithTextAndSummary]:
1235
1352
  ...
1236
1353
 
@@ -1252,13 +1369,13 @@ class Exa:
1252
1369
  exclude_text: Optional[List[str]] = None,
1253
1370
  exclude_source_domain: Optional[bool] = None,
1254
1371
  category: Optional[str] = None,
1255
- livecrawl_timeout: Optional[int] = None,
1372
+ flags: Optional[List[str]] = None,
1256
1373
  subpages: Optional[int] = None,
1257
1374
  subpage_target: Optional[Union[str, List[str]]] = None,
1375
+ livecrawl_timeout: Optional[int] = None,
1258
1376
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1259
1377
  filter_empty_results: Optional[bool] = None,
1260
1378
  extras: Optional[ExtrasOptions] = None,
1261
- flags: Optional[List[str]] = None,
1262
1379
  ) -> SearchResponse[ResultWithHighlightsAndSummary]:
1263
1380
  ...
1264
1381
 
@@ -1281,20 +1398,24 @@ class Exa:
1281
1398
  exclude_text: Optional[List[str]] = None,
1282
1399
  exclude_source_domain: Optional[bool] = None,
1283
1400
  category: Optional[str] = None,
1401
+ flags: Optional[List[str]] = None,
1284
1402
  livecrawl_timeout: Optional[int] = None,
1403
+ livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1404
+ filter_empty_results: Optional[bool] = None,
1285
1405
  subpages: Optional[int] = None,
1286
1406
  subpage_target: Optional[Union[str, List[str]]] = None,
1287
- filter_empty_results: Optional[bool] = None,
1288
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1289
1407
  extras: Optional[ExtrasOptions] = None,
1290
- flags: Optional[List[str]] = None,
1291
1408
  ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
1292
1409
  ...
1293
1410
 
1294
1411
  def find_similar_and_contents(self, url: str, **kwargs):
1295
1412
  options = {k: v for k, v in {"url": url, **kwargs}.items() if v is not None}
1296
1413
  # Default to text if none specified
1297
- if "text" not in options and "highlights" not in options and "summary" not in options:
1414
+ if (
1415
+ "text" not in options
1416
+ and "highlights" not in options
1417
+ and "summary" not in options
1418
+ ):
1298
1419
  options["text"] = True
1299
1420
 
1300
1421
  validate_search_options(
@@ -1324,9 +1445,9 @@ class Exa:
1324
1445
  data = self.request("/findSimilar", options)
1325
1446
  return SearchResponse(
1326
1447
  [Result(**to_snake_case(result)) for result in data["results"]],
1327
- data["autopromptString"] if "autopromptString" in data else None,
1328
- data["resolvedSearchType"] if "resolvedSearchType" in data else None,
1329
- data["autoDate"] if "autoDate" in data else None,
1448
+ data.get("autopromptString"),
1449
+ data.get("resolvedSearchType"),
1450
+ data.get("autoDate"),
1330
1451
  )
1331
1452
 
1332
1453
  def wrap(self, client: OpenAI):
@@ -1400,7 +1521,7 @@ class Exa:
1400
1521
  exa_kwargs=exa_kwargs,
1401
1522
  )
1402
1523
 
1403
- print("Wrapping OpenAI client with Exa functionality.", type(create_with_rag))
1524
+ print("Wrapping OpenAI client with Exa functionality.")
1404
1525
  client.chat.completions.create = create_with_rag # type: ignore
1405
1526
 
1406
1527
  return client
@@ -1444,6 +1565,7 @@ class Exa:
1444
1565
  completion=completion, exa_result=None
1445
1566
  )
1446
1567
 
1568
+ # We do a search_and_contents automatically
1447
1569
  exa_result = self.search_and_contents(query, **exa_kwargs)
1448
1570
  exa_str = format_exa_result(exa_result, max_len=max_len)
1449
1571
  new_messages = add_message_to_messages(completion, messages, exa_str)
@@ -1453,3 +1575,51 @@ class Exa:
1453
1575
  completion=completion, exa_result=exa_result
1454
1576
  )
1455
1577
  return exa_completion
1578
+
1579
+ @overload
1580
+ def answer(
1581
+ self,
1582
+ query: str,
1583
+ *,
1584
+ expanded_queries_limit: Optional[int] = 1,
1585
+ stream: Optional[bool] = False,
1586
+ include_text: Optional[bool] = False,
1587
+ ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1588
+ ...
1589
+
1590
+ def answer(
1591
+ self,
1592
+ query: str,
1593
+ *,
1594
+ expanded_queries_limit: Optional[int] = 1,
1595
+ stream: Optional[bool] = False,
1596
+ include_text: Optional[bool] = False,
1597
+ ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1598
+ """Generate an answer to a query using Exa's search and LLM capabilities.
1599
+
1600
+ Args:
1601
+ query (str): The query to answer.
1602
+ expanded_queries_limit (int, optional): Maximum number of query variations (0-4). Defaults to 1.
1603
+ stream (bool, optional): Whether to stream the response. Defaults to False.
1604
+ include_text (bool, optional): Whether to include full text in the results. Defaults to False.
1605
+
1606
+ Returns:
1607
+ Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1608
+ - If stream=False, returns an AnswerResponse object containing the answer and sources.
1609
+ - If stream=True, returns an iterator that yields either answer chunks or sources.
1610
+ """
1611
+ options = {
1612
+ k: v
1613
+ for k, v in locals().items()
1614
+ if k != "self" and v is not None
1615
+ }
1616
+ options = to_camel_case(options)
1617
+ response = self.request("/answer", options)
1618
+
1619
+ if stream:
1620
+ return response
1621
+
1622
+ return AnswerResponse(
1623
+ response["answer"],
1624
+ [AnswerResult(**to_snake_case(result)) for result in response["sources"]]
1625
+ )
@@ -1,19 +1,19 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: exa-py
3
- Version: 1.7.3
3
+ Version: 1.8.4
4
4
  Summary: Python SDK for Exa API.
5
- Author: Exa AI
5
+ Home-page: https://github.com/exa-labs/exa-py
6
+ Author: Exa
6
7
  Author-email: hello@exa.ai
7
- Requires-Python: >=3.9,<4.0
8
- Classifier: Programming Language :: Python :: 3
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
18
 
19
19
  # Exa
@@ -87,6 +87,18 @@ exa = Exa(api_key="your-api-key")
87
87
  results = exa.get_contents(["urls"],
88
88
  text={"include_html_tags": True, "max_characters": 1000},
89
89
  highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
90
- ```
91
90
 
91
+ # basic answer
92
+ response = exa.answer("This is a query to answer a question")
93
+
94
+ # answer with expanded queries and full text
95
+ response = exa.answer("This is a query to answer a question", expanded_queries_limit=3, include_text=True)
96
+
97
+ # answer with streaming
98
+ response = exa.answer("This is a query to answer with streaming:", stream=True)
99
+
100
+ # Print each chunk as it arrives when answer streaming is enabled
101
+ for chunk in response:
102
+ print(chunk)
103
+ ```
92
104
 
@@ -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,3 @@
1
+ requests
2
+ typing-extensions
3
+ openai>=1.10.0
@@ -0,0 +1 @@
1
+ exa_py
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "exa-py"
3
- version = "1.7.3"
3
+ version = "1.8.3"
4
4
  description = "Python SDK for Exa API."
5
5
  authors = ["Exa AI <hello@exa.ai>"]
6
6
  readme = "README.md"
exa_py-1.8.4/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
exa_py-1.8.4/setup.py ADDED
@@ -0,0 +1,30 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="exa_py",
5
+ version="1.8.4",
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=[
15
+ "requests",
16
+ "typing-extensions",
17
+ "openai>=1.10.0"
18
+ ],
19
+ classifiers=[
20
+ "Development Status :: 5 - Production/Stable",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Typing :: Typed",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ ],
30
+ )
File without changes
File without changes
File without changes