exa-py 1.8.8__py3-none-any.whl → 1.8.9__py3-none-any.whl

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/api.py CHANGED
@@ -1,37 +1,39 @@
1
1
  from __future__ import annotations
2
- from dataclasses import dataclass
2
+
3
3
  import dataclasses
4
- from functools import wraps
4
+ import json
5
+ import os
5
6
  import re
6
- import requests
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
- Iterator,
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": [list], # Domains to search from; exclusive with 'exclude_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": [str], # Results after this publish date; excludes links with no date. ISO 8601 format.
110
- "end_published_date": [str], # Results before this publish date; excludes links with no date. ISO 8601 format.
111
- "include_text": [list], # Must be present in webpage text. (One string, up to 5 words)
112
- "exclude_text": [list], # Must not be present in webpage text. (One string, up to 5 words)
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": [str], # A data category to focus on: 'company', 'research paper', 'news', 'pdf', 'github', 'tweet', 'personal site', 'linkedin profile', 'financial report'
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
- id: str
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['id']
527
- self.url = kwargs['url']
528
- self.title = kwargs.get('title')
529
- self.published_date = kwargs.get('published_date')
530
- self.author = kwargs.get('author')
531
- self.text = kwargs.get('text')
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 "citations" in chunk and chunk["citations"] and chunk["citations"] != "null":
625
- citations = [AnswerResult(**to_snake_case(s)) for s in chunk["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.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(self.base_url + endpoint, json=data, headers=self.headers, stream=True)
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(f"Request failed with status code {res.status_code}: {res.text}")
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
-
@@ -1,20 +1,23 @@
1
- Metadata-Version: 2.3
2
- Name: exa-py
3
- Version: 1.8.8
1
+ Metadata-Version: 2.1
2
+ Name: exa_py
3
+ Version: 1.8.9
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
+ 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
-
@@ -0,0 +1,8 @@
1
+ exa_py/__init__.py,sha256=1selemczpRm1y8V9cWNm90LARnU1jbtyp-Qpx3c7cTw,28
2
+ exa_py/api.py,sha256=BSZ-uSRYfCyHWBMUpV4SV-2yaWustPS7xc63gArbJSE,65156
3
+ exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
5
+ exa_py-1.8.9.dist-info/METADATA,sha256=O_ivBX4PUV2yWQPL69nSmXlqP6S0BTlTYiVMpz4nLAM,3522
6
+ exa_py-1.8.9.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
+ exa_py-1.8.9.dist-info/top_level.txt,sha256=Mfkmscdw9HWR1PtVhU1gAiVo6DHu_tyiVdb89gfZBVI,7
8
+ exa_py-1.8.9.dist-info/RECORD,,
@@ -1,4 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.0.1
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ exa_py
@@ -1,7 +0,0 @@
1
- exa_py/__init__.py,sha256=1selemczpRm1y8V9cWNm90LARnU1jbtyp-Qpx3c7cTw,28
2
- exa_py/api.py,sha256=yzQkUJ94RbnMglVjpevmwuixvKnkwhN_r6KuVV9j3Sw,63279
3
- exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
5
- exa_py-1.8.8.dist-info/METADATA,sha256=UV9_4Df_l1JP-SPRFfryyg7SP7VwYNZ3Iz34FsIXP3M,3419
6
- exa_py-1.8.8.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
7
- exa_py-1.8.8.dist-info/RECORD,,