exa-py 1.16.0__py3-none-any.whl → 2.0.0__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
@@ -47,6 +47,9 @@ from .research import ResearchClient, AsyncResearchClient
47
47
 
48
48
  is_beta = os.getenv("IS_BETA") == "True"
49
49
 
50
+ # Default max characters for text contents
51
+ DEFAULT_MAX_CHARACTERS = 10_000
52
+
50
53
 
51
54
  def snake_to_camel(snake_str: str) -> str:
52
55
  """Convert snake_case string to camelCase.
@@ -141,7 +144,6 @@ SEARCH_OPTIONS_TYPES = {
141
144
  "exclude_text": [
142
145
  list
143
146
  ], # Must not be present in webpage text. (One string, up to 5 words)
144
- "use_autoprompt": [bool], # Convert query to Exa. (Default: false)
145
147
  "type": [
146
148
  str
147
149
  ], # 'keyword', 'neural', 'hybrid', 'fast', 'deep', or 'auto' (Default: auto)
@@ -150,6 +152,7 @@ SEARCH_OPTIONS_TYPES = {
150
152
  ], # A data category to focus on: 'company', 'research paper', 'news', 'pdf', 'github', 'tweet', 'personal site', 'linkedin profile', 'financial report'
151
153
  "flags": [list], # Experimental flags array for Exa usage.
152
154
  "moderation": [bool], # If true, moderate search results for safety.
155
+ "contents": [dict, bool], # Options for retrieving page contents
153
156
  }
154
157
 
155
158
  FIND_SIMILAR_OPTIONS_TYPES = {
@@ -166,6 +169,7 @@ FIND_SIMILAR_OPTIONS_TYPES = {
166
169
  "exclude_source_domain": [bool],
167
170
  "category": [str],
168
171
  "flags": [list], # Experimental flags array for Exa usage.
172
+ "contents": [dict, bool], # Options for retrieving page contents
169
173
  }
170
174
 
171
175
  # the livecrawl options
@@ -174,7 +178,6 @@ LIVECRAWL_OPTIONS = Literal["always", "fallback", "never", "auto", "preferred"]
174
178
  CONTENTS_OPTIONS_TYPES = {
175
179
  "urls": [list],
176
180
  "text": [dict, bool],
177
- "highlights": [dict, bool],
178
181
  "summary": [dict, bool],
179
182
  "context": [dict, bool],
180
183
  "metadata": [dict, bool],
@@ -255,24 +258,6 @@ class TextContentsOptions(TypedDict, total=False):
255
258
  include_html_tags: bool
256
259
 
257
260
 
258
- class HighlightsContentsOptions(TypedDict, total=False):
259
- """A class representing the options that you can specify when requesting highlights
260
-
261
- Attributes:
262
- query (str): The query string for the highlights.
263
- num_sentences (int): Size of highlights to return, in sentences. Default: 5
264
- highlights_per_url (int): Number of highlights to return per URL. Default: 1
265
-
266
- NOTE: When using the "deep" search type, only the default highlights=True is supported.
267
- These options will NOT be respected. Highlights will always be based on the user's query,
268
- and the number and length may vary.
269
- """
270
-
271
- query: str
272
- num_sentences: int
273
- highlights_per_url: int
274
-
275
-
276
261
  class JSONSchema(TypedDict, total=False):
277
262
  """Represents a JSON Schema definition used for structured summary output.
278
263
 
@@ -341,7 +326,6 @@ class CostDollarsContents(TypedDict, total=False):
341
326
  """Represents the cost breakdown for contents."""
342
327
 
343
328
  text: float
344
- highlights: float
345
329
  summary: float
346
330
 
347
331
 
@@ -424,18 +408,14 @@ class _Result:
424
408
  @dataclass
425
409
  class Result(_Result):
426
410
  """
427
- A class representing a search result with optional text, highlights, summary.
411
+ A class representing a search result with optional text and summary.
428
412
 
429
413
  Attributes:
430
414
  text (str, optional)
431
- highlights (List[str], optional)
432
- highlight_scores (List[float], optional)
433
415
  summary (str, optional)
434
416
  """
435
417
 
436
418
  text: Optional[str] = None
437
- highlights: Optional[List[str]] = None
438
- highlight_scores: Optional[List[float]] = None
439
419
  summary: Optional[str] = None
440
420
 
441
421
  def __init__(
@@ -451,9 +431,9 @@ class Result(_Result):
451
431
  subpages=None,
452
432
  extras=None,
453
433
  text=None,
454
- highlights=None,
455
- highlight_scores=None,
456
434
  summary=None,
435
+ highlights=None, # Deprecated, for backward compatibility
436
+ highlight_scores=None, # Deprecated, for backward compatibility
457
437
  ):
458
438
  super().__init__(
459
439
  url,
@@ -468,18 +448,11 @@ class Result(_Result):
468
448
  extras,
469
449
  )
470
450
  self.text = text
471
- self.highlights = highlights
472
- self.highlight_scores = highlight_scores
473
451
  self.summary = summary
474
452
 
475
453
  def __str__(self):
476
454
  base_str = super().__str__()
477
- return base_str + (
478
- f"Text: {self.text}\n"
479
- f"Highlights: {self.highlights}\n"
480
- f"Highlight Scores: {self.highlight_scores}\n"
481
- f"Summary: {self.summary}\n"
482
- )
455
+ return base_str + (f"Text: {self.text}\nSummary: {self.summary}\n")
483
456
 
484
457
 
485
458
  @dataclass
@@ -526,113 +499,6 @@ class ResultWithText(_Result):
526
499
  return base_str + f"Text: {self.text}\n"
527
500
 
528
501
 
529
- @dataclass
530
- class ResultWithHighlights(_Result):
531
- """
532
- A class representing a search result with highlights present.
533
-
534
- Attributes:
535
- highlights (List[str])
536
- highlight_scores (List[float])
537
- """
538
-
539
- highlights: List[str] = dataclasses.field(default_factory=list)
540
- highlight_scores: List[float] = dataclasses.field(default_factory=list)
541
-
542
- def __init__(
543
- self,
544
- url,
545
- id,
546
- title=None,
547
- score=None,
548
- published_date=None,
549
- author=None,
550
- image=None,
551
- favicon=None,
552
- subpages=None,
553
- extras=None,
554
- highlights=None,
555
- highlight_scores=None,
556
- ):
557
- super().__init__(
558
- url,
559
- id,
560
- title,
561
- score,
562
- published_date,
563
- author,
564
- image,
565
- favicon,
566
- subpages,
567
- extras,
568
- )
569
- self.highlights = highlights if highlights is not None else []
570
- self.highlight_scores = highlight_scores if highlight_scores is not None else []
571
-
572
- def __str__(self):
573
- base_str = super().__str__()
574
- return base_str + (
575
- f"Highlights: {self.highlights}\n"
576
- f"Highlight Scores: {self.highlight_scores}\n"
577
- )
578
-
579
-
580
- @dataclass
581
- class ResultWithTextAndHighlights(_Result):
582
- """
583
- A class representing a search result with text and highlights present.
584
-
585
- Attributes:
586
- text (str)
587
- highlights (List[str])
588
- highlight_scores (List[float])
589
- """
590
-
591
- text: str = dataclasses.field(default_factory=str)
592
- highlights: List[str] = dataclasses.field(default_factory=list)
593
- highlight_scores: List[float] = dataclasses.field(default_factory=list)
594
-
595
- def __init__(
596
- self,
597
- url,
598
- id,
599
- title=None,
600
- score=None,
601
- published_date=None,
602
- author=None,
603
- image=None,
604
- favicon=None,
605
- subpages=None,
606
- extras=None,
607
- text="",
608
- highlights=None,
609
- highlight_scores=None,
610
- ):
611
- super().__init__(
612
- url,
613
- id,
614
- title,
615
- score,
616
- published_date,
617
- author,
618
- image,
619
- favicon,
620
- subpages,
621
- extras,
622
- )
623
- self.text = text
624
- self.highlights = highlights if highlights is not None else []
625
- self.highlight_scores = highlight_scores if highlight_scores is not None else []
626
-
627
- def __str__(self):
628
- base_str = super().__str__()
629
- return base_str + (
630
- f"Text: {self.text}\n"
631
- f"Highlights: {self.highlights}\n"
632
- f"Highlight Scores: {self.highlight_scores}\n"
633
- )
634
-
635
-
636
502
  @dataclass
637
503
  class ResultWithSummary(_Result):
638
504
  """
@@ -725,123 +591,6 @@ class ResultWithTextAndSummary(_Result):
725
591
  return base_str + f"Text: {self.text}\n" + f"Summary: {self.summary}\n"
726
592
 
727
593
 
728
- @dataclass
729
- class ResultWithHighlightsAndSummary(_Result):
730
- """
731
- A class representing a search result with highlights and summary present.
732
-
733
- Attributes:
734
- highlights (List[str])
735
- highlight_scores (List[float])
736
- summary (str)
737
- """
738
-
739
- highlights: List[str] = dataclasses.field(default_factory=list)
740
- highlight_scores: List[float] = dataclasses.field(default_factory=list)
741
- summary: str = dataclasses.field(default_factory=str)
742
-
743
- def __init__(
744
- self,
745
- url,
746
- id,
747
- title=None,
748
- score=None,
749
- published_date=None,
750
- author=None,
751
- image=None,
752
- favicon=None,
753
- subpages=None,
754
- extras=None,
755
- highlights=None,
756
- highlight_scores=None,
757
- summary="",
758
- ):
759
- super().__init__(
760
- url,
761
- id,
762
- title,
763
- score,
764
- published_date,
765
- author,
766
- image,
767
- favicon,
768
- subpages,
769
- extras,
770
- )
771
- self.highlights = highlights if highlights is not None else []
772
- self.highlight_scores = highlight_scores if highlight_scores is not None else []
773
- self.summary = summary
774
-
775
- def __str__(self):
776
- base_str = super().__str__()
777
- return base_str + (
778
- f"Highlights: {self.highlights}\n"
779
- f"Highlight Scores: {self.highlight_scores}\n"
780
- f"Summary: {self.summary}\n"
781
- )
782
-
783
-
784
- @dataclass
785
- class ResultWithTextAndHighlightsAndSummary(_Result):
786
- """
787
- A class representing a search result with text, highlights, and summary present.
788
-
789
- Attributes:
790
- text (str)
791
- highlights (List[str])
792
- highlight_scores (List[float])
793
- summary (str)
794
- """
795
-
796
- text: str = dataclasses.field(default_factory=str)
797
- highlights: List[str] = dataclasses.field(default_factory=list)
798
- highlight_scores: List[float] = dataclasses.field(default_factory=list)
799
- summary: str = dataclasses.field(default_factory=str)
800
-
801
- def __init__(
802
- self,
803
- url,
804
- id,
805
- title=None,
806
- score=None,
807
- published_date=None,
808
- author=None,
809
- image=None,
810
- favicon=None,
811
- subpages=None,
812
- extras=None,
813
- text="",
814
- highlights=None,
815
- highlight_scores=None,
816
- summary="",
817
- ):
818
- super().__init__(
819
- url,
820
- id,
821
- title,
822
- score,
823
- published_date,
824
- author,
825
- image,
826
- favicon,
827
- subpages,
828
- extras,
829
- )
830
- self.text = text
831
- self.highlights = highlights if highlights is not None else []
832
- self.highlight_scores = highlight_scores if highlight_scores is not None else []
833
- self.summary = summary
834
-
835
- def __str__(self):
836
- base_str = super().__str__()
837
- return base_str + (
838
- f"Text: {self.text}\n"
839
- f"Highlights: {self.highlights}\n"
840
- f"Highlight Scores: {self.highlight_scores}\n"
841
- f"Summary: {self.summary}\n"
842
- )
843
-
844
-
845
594
  @dataclass
846
595
  class AnswerResult:
847
596
  """A class representing a result for an answer.
@@ -1073,7 +822,6 @@ class SearchResponse(Generic[T]):
1073
822
 
1074
823
  Attributes:
1075
824
  results (List[Result]): A list of search results.
1076
- autoprompt_string (str, optional): The Exa query created by autoprompt.
1077
825
  resolved_search_type (str, optional): 'neural' or 'keyword' if auto.
1078
826
  auto_date (str, optional): A date for filtering if autoprompt found one.
1079
827
  context (str, optional): Combined context string when requested via contents.context.
@@ -1082,7 +830,6 @@ class SearchResponse(Generic[T]):
1082
830
  """
1083
831
 
1084
832
  results: List[T]
1085
- autoprompt_string: Optional[str]
1086
833
  resolved_search_type: Optional[str]
1087
834
  auto_date: Optional[str]
1088
835
  context: Optional[str] = None
@@ -1093,8 +840,6 @@ class SearchResponse(Generic[T]):
1093
840
  output = "\n\n".join(str(result) for result in self.results)
1094
841
  if self.context:
1095
842
  output += f"\nContext: {self.context}"
1096
- if self.autoprompt_string:
1097
- output += f"\n\nAutoprompt String: {self.autoprompt_string}"
1098
843
  if self.resolved_search_type:
1099
844
  output += f"\nResolved Search Type: {self.resolved_search_type}"
1100
845
  if self.cost_dollars:
@@ -1230,312 +975,28 @@ class Exa:
1230
975
  return res
1231
976
  else:
1232
977
  res = requests.post(
1233
- self.base_url + endpoint, data=json_data, headers=request_headers
1234
- )
1235
- elif method.upper() == "PATCH":
1236
- res = requests.patch(
1237
- self.base_url + endpoint, data=json_data, headers=request_headers
1238
- )
1239
- elif method.upper() == "DELETE":
1240
- res = requests.delete(self.base_url + endpoint, headers=request_headers)
1241
- else:
1242
- raise ValueError(f"Unsupported HTTP method: {method}")
1243
-
1244
- if res.status_code >= 400:
1245
- raise ValueError(
1246
- f"Request failed with status code {res.status_code}: {res.text}"
1247
- )
1248
- return res.json()
1249
-
1250
- def search(
1251
- self,
1252
- query: str,
1253
- *,
1254
- num_results: Optional[int] = None,
1255
- include_domains: Optional[List[str]] = None,
1256
- exclude_domains: Optional[List[str]] = None,
1257
- start_crawl_date: Optional[str] = None,
1258
- end_crawl_date: Optional[str] = None,
1259
- start_published_date: Optional[str] = None,
1260
- end_published_date: Optional[str] = None,
1261
- include_text: Optional[List[str]] = None,
1262
- exclude_text: Optional[List[str]] = None,
1263
- use_autoprompt: Optional[bool] = None,
1264
- type: Optional[str] = None,
1265
- category: Optional[str] = None,
1266
- flags: Optional[List[str]] = None,
1267
- moderation: Optional[bool] = None,
1268
- user_location: Optional[str] = None,
1269
- ) -> SearchResponse[_Result]:
1270
- """Perform a search with a prompt-engineered query to retrieve relevant results.
1271
-
1272
- Args:
1273
- query (str): The query string.
1274
- num_results (int, optional): Number of search results to return (default 10).
1275
- include_domains (List[str], optional): Domains to include in the search.
1276
- exclude_domains (List[str], optional): Domains to exclude from the search.
1277
- start_crawl_date (str, optional): Only links crawled after this date.
1278
- end_crawl_date (str, optional): Only links crawled before this date.
1279
- start_published_date (str, optional): Only links published after this date.
1280
- end_published_date (str, optional): Only links published before this date.
1281
- include_text (List[str], optional): Strings that must appear in the page text.
1282
- exclude_text (List[str], optional): Strings that must not appear in the page text.
1283
- use_autoprompt (bool, optional): Convert query to Exa (default False).
1284
- type (str, optional): 'keyword', 'neural', 'hybrid', 'fast', 'deep', or 'auto' (default 'auto').
1285
- category (str, optional): e.g. 'company'
1286
- flags (List[str], optional): Experimental flags for Exa usage.
1287
- moderation (bool, optional): If True, the search results will be moderated for safety.
1288
- user_location (str, optional): Two-letter ISO country code of the user (e.g. US).
1289
-
1290
- Returns:
1291
- SearchResponse: The response containing search results, etc.
1292
- """
1293
- options = {k: v for k, v in locals().items() if k != "self" and v is not None}
1294
- validate_search_options(options, SEARCH_OPTIONS_TYPES)
1295
- options = to_camel_case(options)
1296
- data = self.request("/search", options)
1297
- cost_dollars = parse_cost_dollars(data.get("costDollars"))
1298
- results = []
1299
- for result in data["results"]:
1300
- snake_result = to_snake_case(result)
1301
- results.append(
1302
- Result(
1303
- url=snake_result.get("url"),
1304
- id=snake_result.get("id"),
1305
- title=snake_result.get("title"),
1306
- score=snake_result.get("score"),
1307
- published_date=snake_result.get("published_date"),
1308
- author=snake_result.get("author"),
1309
- image=snake_result.get("image"),
1310
- favicon=snake_result.get("favicon"),
1311
- subpages=snake_result.get("subpages"),
1312
- extras=snake_result.get("extras"),
1313
- text=snake_result.get("text"),
1314
- highlights=snake_result.get("highlights"),
1315
- highlight_scores=snake_result.get("highlight_scores"),
1316
- summary=snake_result.get("summary"),
1317
- )
1318
- )
1319
- return SearchResponse(
1320
- results,
1321
- data["autopromptString"] if "autopromptString" in data else None,
1322
- data["resolvedSearchType"] if "resolvedSearchType" in data else None,
1323
- data["autoDate"] if "autoDate" in data else None,
1324
- cost_dollars=cost_dollars,
1325
- )
1326
-
1327
- @overload
1328
- def search_and_contents(
1329
- self,
1330
- query: str,
1331
- *,
1332
- num_results: Optional[int] = None,
1333
- include_domains: Optional[List[str]] = None,
1334
- exclude_domains: Optional[List[str]] = None,
1335
- start_crawl_date: Optional[str] = None,
1336
- end_crawl_date: Optional[str] = None,
1337
- start_published_date: Optional[str] = None,
1338
- end_published_date: Optional[str] = None,
1339
- include_text: Optional[List[str]] = None,
1340
- exclude_text: Optional[List[str]] = None,
1341
- use_autoprompt: Optional[bool] = None,
1342
- type: Optional[str] = None,
1343
- category: Optional[str] = None,
1344
- flags: Optional[List[str]] = None,
1345
- moderation: Optional[bool] = None,
1346
- user_location: Optional[str] = None,
1347
- livecrawl_timeout: Optional[int] = None,
1348
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1349
- filter_empty_results: Optional[bool] = None,
1350
- subpages: Optional[int] = None,
1351
- subpage_target: Optional[Union[str, List[str]]] = None,
1352
- extras: Optional[ExtrasOptions] = None,
1353
- ) -> SearchResponse[ResultWithText]: ...
1354
-
1355
- @overload
1356
- def search_and_contents(
1357
- self,
1358
- query: str,
1359
- *,
1360
- text: Union[TextContentsOptions, Literal[True]],
1361
- num_results: Optional[int] = None,
1362
- include_domains: Optional[List[str]] = None,
1363
- exclude_domains: Optional[List[str]] = None,
1364
- start_crawl_date: Optional[str] = None,
1365
- end_crawl_date: Optional[str] = None,
1366
- start_published_date: Optional[str] = None,
1367
- end_published_date: Optional[str] = None,
1368
- include_text: Optional[List[str]] = None,
1369
- exclude_text: Optional[List[str]] = None,
1370
- use_autoprompt: Optional[bool] = None,
1371
- type: Optional[str] = None,
1372
- category: Optional[str] = None,
1373
- flags: Optional[List[str]] = None,
1374
- moderation: Optional[bool] = None,
1375
- subpages: Optional[int] = None,
1376
- livecrawl_timeout: Optional[int] = None,
1377
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1378
- filter_empty_results: Optional[bool] = None,
1379
- subpage_target: Optional[Union[str, List[str]]] = None,
1380
- extras: Optional[ExtrasOptions] = None,
1381
- ) -> SearchResponse[ResultWithText]: ...
1382
-
1383
- @overload
1384
- def search_and_contents(
1385
- self,
1386
- query: str,
1387
- *,
1388
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1389
- num_results: Optional[int] = None,
1390
- include_domains: Optional[List[str]] = None,
1391
- exclude_domains: Optional[List[str]] = None,
1392
- start_crawl_date: Optional[str] = None,
1393
- end_crawl_date: Optional[str] = None,
1394
- start_published_date: Optional[str] = None,
1395
- end_published_date: Optional[str] = None,
1396
- include_text: Optional[List[str]] = None,
1397
- exclude_text: Optional[List[str]] = None,
1398
- use_autoprompt: Optional[bool] = None,
1399
- type: Optional[str] = None,
1400
- category: Optional[str] = None,
1401
- subpages: Optional[int] = None,
1402
- subpage_target: Optional[Union[str, List[str]]] = None,
1403
- flags: Optional[List[str]] = None,
1404
- moderation: Optional[bool] = None,
1405
- user_location: Optional[str] = None,
1406
- livecrawl_timeout: Optional[int] = None,
1407
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1408
- filter_empty_results: Optional[bool] = None,
1409
- extras: Optional[ExtrasOptions] = None,
1410
- ) -> SearchResponse[ResultWithHighlights]: ...
1411
-
1412
- @overload
1413
- def search_and_contents(
1414
- self,
1415
- query: str,
1416
- *,
1417
- text: Union[TextContentsOptions, Literal[True]],
1418
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1419
- num_results: Optional[int] = None,
1420
- include_domains: Optional[List[str]] = None,
1421
- exclude_domains: Optional[List[str]] = None,
1422
- start_crawl_date: Optional[str] = None,
1423
- end_crawl_date: Optional[str] = None,
1424
- start_published_date: Optional[str] = None,
1425
- end_published_date: Optional[str] = None,
1426
- include_text: Optional[List[str]] = None,
1427
- exclude_text: Optional[List[str]] = None,
1428
- use_autoprompt: Optional[bool] = None,
1429
- type: Optional[str] = None,
1430
- category: Optional[str] = None,
1431
- subpages: Optional[int] = None,
1432
- subpage_target: Optional[Union[str, List[str]]] = None,
1433
- flags: Optional[List[str]] = None,
1434
- moderation: Optional[bool] = None,
1435
- user_location: Optional[str] = None,
1436
- livecrawl_timeout: Optional[int] = None,
1437
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1438
- filter_empty_results: Optional[bool] = None,
1439
- extras: Optional[ExtrasOptions] = None,
1440
- ) -> SearchResponse[ResultWithTextAndHighlights]: ...
1441
-
1442
- @overload
1443
- def search_and_contents(
1444
- self,
1445
- query: str,
1446
- *,
1447
- summary: Union[SummaryContentsOptions, Literal[True]],
1448
- num_results: Optional[int] = None,
1449
- include_domains: Optional[List[str]] = None,
1450
- exclude_domains: Optional[List[str]] = None,
1451
- start_crawl_date: Optional[str] = None,
1452
- end_crawl_date: Optional[str] = None,
1453
- start_published_date: Optional[str] = None,
1454
- end_published_date: Optional[str] = None,
1455
- include_text: Optional[List[str]] = None,
1456
- exclude_text: Optional[List[str]] = None,
1457
- use_autoprompt: Optional[bool] = None,
1458
- type: Optional[str] = None,
1459
- category: Optional[str] = None,
1460
- subpages: Optional[int] = None,
1461
- subpage_target: Optional[Union[str, List[str]]] = None,
1462
- flags: Optional[List[str]] = None,
1463
- moderation: Optional[bool] = None,
1464
- user_location: Optional[str] = None,
1465
- livecrawl_timeout: Optional[int] = None,
1466
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1467
- filter_empty_results: Optional[bool] = None,
1468
- extras: Optional[ExtrasOptions] = None,
1469
- ) -> SearchResponse[ResultWithSummary]: ...
1470
-
1471
- @overload
1472
- def search_and_contents(
1473
- self,
1474
- query: str,
1475
- *,
1476
- text: Union[TextContentsOptions, Literal[True]],
1477
- summary: Union[SummaryContentsOptions, Literal[True]],
1478
- num_results: Optional[int] = None,
1479
- include_domains: Optional[List[str]] = None,
1480
- exclude_domains: Optional[List[str]] = None,
1481
- start_crawl_date: Optional[str] = None,
1482
- end_crawl_date: Optional[str] = None,
1483
- start_published_date: Optional[str] = None,
1484
- end_published_date: Optional[str] = None,
1485
- include_text: Optional[List[str]] = None,
1486
- exclude_text: Optional[List[str]] = None,
1487
- use_autoprompt: Optional[bool] = None,
1488
- type: Optional[str] = None,
1489
- category: Optional[str] = None,
1490
- subpages: Optional[int] = None,
1491
- subpage_target: Optional[Union[str, List[str]]] = None,
1492
- flags: Optional[List[str]] = None,
1493
- moderation: Optional[bool] = None,
1494
- user_location: Optional[str] = None,
1495
- livecrawl_timeout: Optional[int] = None,
1496
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1497
- filter_empty_results: Optional[bool] = None,
1498
- extras: Optional[ExtrasOptions] = None,
1499
- ) -> SearchResponse[ResultWithTextAndSummary]: ...
1500
-
1501
- @overload
1502
- def search_and_contents(
1503
- self,
1504
- query: str,
1505
- *,
1506
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1507
- summary: Union[SummaryContentsOptions, Literal[True]],
1508
- num_results: Optional[int] = None,
1509
- include_domains: Optional[List[str]] = None,
1510
- exclude_domains: Optional[List[str]] = None,
1511
- start_crawl_date: Optional[str] = None,
1512
- end_crawl_date: Optional[str] = None,
1513
- start_published_date: Optional[str] = None,
1514
- end_published_date: Optional[str] = None,
1515
- include_text: Optional[List[str]] = None,
1516
- exclude_text: Optional[List[str]] = None,
1517
- use_autoprompt: Optional[bool] = None,
1518
- type: Optional[str] = None,
1519
- category: Optional[str] = None,
1520
- subpages: Optional[int] = None,
1521
- subpage_target: Optional[Union[str, List[str]]] = None,
1522
- flags: Optional[List[str]] = None,
1523
- moderation: Optional[bool] = None,
1524
- user_location: Optional[str] = None,
1525
- livecrawl_timeout: Optional[int] = None,
1526
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1527
- filter_empty_results: Optional[bool] = None,
1528
- extras: Optional[ExtrasOptions] = None,
1529
- ) -> SearchResponse[ResultWithHighlightsAndSummary]: ...
978
+ self.base_url + endpoint, data=json_data, headers=request_headers
979
+ )
980
+ elif method.upper() == "PATCH":
981
+ res = requests.patch(
982
+ self.base_url + endpoint, data=json_data, headers=request_headers
983
+ )
984
+ elif method.upper() == "DELETE":
985
+ res = requests.delete(self.base_url + endpoint, headers=request_headers)
986
+ else:
987
+ raise ValueError(f"Unsupported HTTP method: {method}")
1530
988
 
1531
- @overload
1532
- def search_and_contents(
989
+ if res.status_code >= 400:
990
+ raise ValueError(
991
+ f"Request failed with status code {res.status_code}: {res.text}"
992
+ )
993
+ return res.json()
994
+
995
+ def search(
1533
996
  self,
1534
997
  query: str,
1535
998
  *,
1536
- text: Union[TextContentsOptions, Literal[True]],
1537
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1538
- summary: Union[SummaryContentsOptions, Literal[True]],
999
+ contents: Optional[Union[Dict, bool]] = None,
1539
1000
  num_results: Optional[int] = None,
1540
1001
  include_domains: Optional[List[str]] = None,
1541
1002
  exclude_domains: Optional[List[str]] = None,
@@ -1545,33 +1006,102 @@ class Exa:
1545
1006
  end_published_date: Optional[str] = None,
1546
1007
  include_text: Optional[List[str]] = None,
1547
1008
  exclude_text: Optional[List[str]] = None,
1548
- use_autoprompt: Optional[bool] = None,
1549
1009
  type: Optional[str] = None,
1550
1010
  category: Optional[str] = None,
1551
1011
  flags: Optional[List[str]] = None,
1552
1012
  moderation: Optional[bool] = None,
1553
1013
  user_location: Optional[str] = None,
1554
- livecrawl_timeout: Optional[int] = None,
1555
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1556
- subpages: Optional[int] = None,
1557
- subpage_target: Optional[Union[str, List[str]]] = None,
1558
- filter_empty_results: Optional[bool] = None,
1559
- extras: Optional[ExtrasOptions] = None,
1560
- ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...
1014
+ ) -> SearchResponse[Result]:
1015
+ """Perform a search.
1016
+
1017
+ By default, returns text contents with 10,000 max characters. Use contents=False to opt-out.
1018
+
1019
+ Args:
1020
+ query (str): The query string.
1021
+ contents (dict | bool, optional): Options for retrieving page contents.
1022
+ Defaults to {"text": {"maxCharacters": 10000}}. Use False to disable contents.
1023
+ num_results (int, optional): Number of search results to return (default 10).
1024
+ include_domains (List[str], optional): Domains to include in the search.
1025
+ exclude_domains (List[str], optional): Domains to exclude from the search.
1026
+ start_crawl_date (str, optional): Only links crawled after this date.
1027
+ end_crawl_date (str, optional): Only links crawled before this date.
1028
+ start_published_date (str, optional): Only links published after this date.
1029
+ end_published_date (str, optional): Only links published before this date.
1030
+ include_text (List[str], optional): Strings that must appear in the page text.
1031
+ exclude_text (List[str], optional): Strings that must not appear in the page text.
1032
+ type (str, optional): 'keyword', 'neural', 'hybrid', 'fast', 'deep', or 'auto' (default 'auto').
1033
+ category (str, optional): e.g. 'company'
1034
+ flags (List[str], optional): Experimental flags for Exa usage.
1035
+ moderation (bool, optional): If True, the search results will be moderated for safety.
1036
+ user_location (str, optional): Two-letter ISO country code of the user (e.g. US).
1037
+
1038
+ Returns:
1039
+ SearchResponse: The response containing search results, etc.
1040
+ """
1041
+ options = {k: v for k, v in locals().items() if k != "self" and v is not None}
1042
+
1043
+ # Handle contents parameter with default behavior
1044
+ if contents is False:
1045
+ # Explicitly no contents - remove from options
1046
+ options.pop("contents", None)
1047
+ elif contents is None and "contents" not in options:
1048
+ # No contents specified - add default text with 10,000 max characters
1049
+ options["contents"] = {"text": {"max_characters": DEFAULT_MAX_CHARACTERS}}
1050
+ elif contents is not None:
1051
+ # User provided contents - use as-is
1052
+ options["contents"] = contents
1053
+
1054
+ validate_search_options(options, SEARCH_OPTIONS_TYPES)
1055
+ options = to_camel_case(options)
1056
+ data = self.request("/search", options)
1057
+ cost_dollars = parse_cost_dollars(data.get("costDollars"))
1058
+ results = []
1059
+ for result in data["results"]:
1060
+ snake_result = to_snake_case(result)
1061
+ results.append(
1062
+ Result(
1063
+ url=snake_result.get("url"),
1064
+ id=snake_result.get("id"),
1065
+ title=snake_result.get("title"),
1066
+ score=snake_result.get("score"),
1067
+ published_date=snake_result.get("published_date"),
1068
+ author=snake_result.get("author"),
1069
+ image=snake_result.get("image"),
1070
+ favicon=snake_result.get("favicon"),
1071
+ subpages=snake_result.get("subpages"),
1072
+ extras=snake_result.get("extras"),
1073
+ text=snake_result.get("text"),
1074
+ summary=snake_result.get("summary"),
1075
+ )
1076
+ )
1077
+ return SearchResponse(
1078
+ results,
1079
+ data["resolvedSearchType"] if "resolvedSearchType" in data else None,
1080
+ data["autoDate"] if "autoDate" in data else None,
1081
+ cost_dollars=cost_dollars,
1082
+ )
1561
1083
 
1562
1084
  def search_and_contents(self, query: str, **kwargs):
1085
+ """
1086
+ DEPRECATED: Use search() instead. The search() method now returns text contents by default.
1087
+
1088
+ Migration:
1089
+ - search_and_contents(query) → search(query)
1090
+ - search_and_contents(query, text=True) → search(query, contents={"text": True})
1091
+ - search_and_contents(query, summary=True) → search(query, contents={"summary": True})
1092
+ """
1093
+
1563
1094
  options = {"query": query}
1564
1095
  for k, v in kwargs.items():
1565
1096
  if v is not None:
1566
1097
  options[k] = v
1567
- # If user didn't ask for any particular content, default to text
1098
+ # If user didn't ask for any particular content, default to text with max characters
1568
1099
  if (
1569
1100
  "text" not in options
1570
- and "highlights" not in options
1571
1101
  and "summary" not in options
1572
1102
  and "extras" not in options
1573
1103
  ):
1574
- options["text"] = True
1104
+ options["text"] = {"max_characters": DEFAULT_MAX_CHARACTERS}
1575
1105
 
1576
1106
  merged_options = {}
1577
1107
  merged_options.update(SEARCH_OPTIONS_TYPES)
@@ -1590,7 +1120,6 @@ class Exa:
1590
1120
  options,
1591
1121
  [
1592
1122
  "text",
1593
- "highlights",
1594
1123
  "summary",
1595
1124
  "context",
1596
1125
  "subpages",
@@ -1620,16 +1149,13 @@ class Exa:
1620
1149
  subpages=snake_result.get("subpages"),
1621
1150
  extras=snake_result.get("extras"),
1622
1151
  text=snake_result.get("text"),
1623
- highlights=snake_result.get("highlights"),
1624
- highlight_scores=snake_result.get("highlight_scores"),
1625
1152
  summary=snake_result.get("summary"),
1626
1153
  )
1627
1154
  )
1628
1155
  return SearchResponse(
1629
1156
  results,
1630
- data["autopromptString"] if "autopromptString" in data else None,
1631
- data["resolvedSearchType"] if "resolvedSearchType" in data else None,
1632
- data["autoDate"] if "autoDate" in data else None,
1157
+ data.get("resolvedSearchType"),
1158
+ data.get("autoDate"),
1633
1159
  context=data.get("context"),
1634
1160
  cost_dollars=cost_dollars,
1635
1161
  )
@@ -1662,37 +1188,6 @@ class Exa:
1662
1188
  flags: Optional[List[str]] = None,
1663
1189
  ) -> SearchResponse[ResultWithText]: ...
1664
1190
 
1665
- @overload
1666
- def get_contents(
1667
- self,
1668
- urls: Union[str, List[str], List[_Result]],
1669
- *,
1670
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1671
- livecrawl_timeout: Optional[int] = None,
1672
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1673
- filter_empty_results: Optional[bool] = None,
1674
- subpages: Optional[int] = None,
1675
- subpage_target: Optional[Union[str, List[str]]] = None,
1676
- extras: Optional[ExtrasOptions] = None,
1677
- flags: Optional[List[str]] = None,
1678
- ) -> SearchResponse[ResultWithHighlights]: ...
1679
-
1680
- @overload
1681
- def get_contents(
1682
- self,
1683
- urls: Union[str, List[str], List[_Result]],
1684
- *,
1685
- text: Union[TextContentsOptions, Literal[True]],
1686
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1687
- livecrawl_timeout: Optional[int] = None,
1688
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1689
- filter_empty_results: Optional[bool] = None,
1690
- subpages: Optional[int] = None,
1691
- subpage_target: Optional[Union[str, List[str]]] = None,
1692
- extras: Optional[ExtrasOptions] = None,
1693
- flags: Optional[List[str]] = None,
1694
- ) -> SearchResponse[ResultWithTextAndHighlights]: ...
1695
-
1696
1191
  @overload
1697
1192
  def get_contents(
1698
1193
  self,
@@ -1724,40 +1219,14 @@ class Exa:
1724
1219
  flags: Optional[List[str]] = None,
1725
1220
  ) -> SearchResponse[ResultWithTextAndSummary]: ...
1726
1221
 
1727
- @overload
1728
- def get_contents(
1729
- self,
1730
- urls: Union[str, List[str], List[_Result]],
1731
- *,
1732
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1733
- summary: Union[SummaryContentsOptions, Literal[True]],
1734
- livecrawl_timeout: Optional[int] = None,
1735
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1736
- filter_empty_results: Optional[bool] = None,
1737
- subpages: Optional[int] = None,
1738
- subpage_target: Optional[Union[str, List[str]]] = None,
1739
- extras: Optional[ExtrasOptions] = None,
1740
- flags: Optional[List[str]] = None,
1741
- ) -> SearchResponse[ResultWithHighlightsAndSummary]: ...
1742
-
1743
- @overload
1744
- def get_contents(
1745
- self,
1746
- urls: Union[str, List[str], List[_Result]],
1747
- *,
1748
- text: Union[TextContentsOptions, Literal[True]],
1749
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1750
- summary: Union[SummaryContentsOptions, Literal[True]],
1751
- livecrawl_timeout: Optional[int] = None,
1752
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1753
- filter_empty_results: Optional[bool] = None,
1754
- subpages: Optional[int] = None,
1755
- subpage_target: Optional[Union[str, List[str]]] = None,
1756
- extras: Optional[ExtrasOptions] = None,
1757
- flags: Optional[List[str]] = None,
1758
- ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...
1759
-
1760
1222
  def get_contents(self, urls: Union[str, List[str], List[_Result]], **kwargs):
1223
+ # Normalize urls to always be a list
1224
+ if isinstance(urls, str):
1225
+ urls = [urls]
1226
+ elif isinstance(urls, list) and len(urls) > 0 and isinstance(urls[0], _Result):
1227
+ # Extract URLs from Result objects
1228
+ urls = [r.url for r in urls]
1229
+
1761
1230
  options = {"urls": urls}
1762
1231
  for k, v in kwargs.items():
1763
1232
  if k != "self" and v is not None:
@@ -1765,11 +1234,10 @@ class Exa:
1765
1234
 
1766
1235
  if (
1767
1236
  "text" not in options
1768
- and "highlights" not in options
1769
1237
  and "summary" not in options
1770
1238
  and "extras" not in options
1771
1239
  ):
1772
- options["text"] = True
1240
+ options["text"] = {"max_characters": DEFAULT_MAX_CHARACTERS}
1773
1241
 
1774
1242
  merged_options = {}
1775
1243
  merged_options.update(CONTENTS_OPTIONS_TYPES)
@@ -1810,14 +1278,11 @@ class Exa:
1810
1278
  subpages=snake_result.get("subpages"),
1811
1279
  extras=snake_result.get("extras"),
1812
1280
  text=snake_result.get("text"),
1813
- highlights=snake_result.get("highlights"),
1814
- highlight_scores=snake_result.get("highlight_scores"),
1815
1281
  summary=snake_result.get("summary"),
1816
1282
  )
1817
1283
  )
1818
1284
  return SearchResponse(
1819
1285
  results,
1820
- data.get("autopromptString"),
1821
1286
  data.get("resolvedSearchType"),
1822
1287
  data.get("autoDate"),
1823
1288
  context=data.get("context"),
@@ -1829,6 +1294,7 @@ class Exa:
1829
1294
  self,
1830
1295
  url: str,
1831
1296
  *,
1297
+ contents: Optional[Union[Dict, bool]] = None,
1832
1298
  num_results: Optional[int] = None,
1833
1299
  include_domains: Optional[List[str]] = None,
1834
1300
  exclude_domains: Optional[List[str]] = None,
@@ -1841,11 +1307,15 @@ class Exa:
1841
1307
  exclude_source_domain: Optional[bool] = None,
1842
1308
  category: Optional[str] = None,
1843
1309
  flags: Optional[List[str]] = None,
1844
- ) -> SearchResponse[_Result]:
1310
+ ) -> SearchResponse[Result]:
1845
1311
  """Finds similar pages to a given URL, potentially with domain filters and date filters.
1846
1312
 
1313
+ By default, returns text contents with 10,000 max characters. Use contents=False to opt-out.
1314
+
1847
1315
  Args:
1848
1316
  url (str): The URL to find similar pages for.
1317
+ contents (dict | bool, optional): Options for retrieving page contents.
1318
+ Defaults to {"text": {"maxCharacters": 10000}}. Use False to disable contents.
1849
1319
  num_results (int, optional): Number of results to return. Default is None (server default).
1850
1320
  include_domains (List[str], optional): Domains to include in the search.
1851
1321
  exclude_domains (List[str], optional): Domains to exclude from the search.
@@ -1860,9 +1330,21 @@ class Exa:
1860
1330
  flags (List[str], optional): Experimental flags.
1861
1331
 
1862
1332
  Returns:
1863
- SearchResponse[_Result]
1333
+ SearchResponse[Result]
1864
1334
  """
1865
1335
  options = {k: v for k, v in locals().items() if k != "self" and v is not None}
1336
+
1337
+ # Handle contents parameter with default behavior
1338
+ if contents is False:
1339
+ # Explicitly no contents - remove from options
1340
+ options.pop("contents", None)
1341
+ elif contents is None and "contents" not in options:
1342
+ # No contents specified - add default text with 10,000 max characters
1343
+ options["contents"] = {"text": {"max_characters": DEFAULT_MAX_CHARACTERS}}
1344
+ elif contents is not None:
1345
+ # User provided contents - use as-is
1346
+ options["contents"] = contents
1347
+
1866
1348
  validate_search_options(options, FIND_SIMILAR_OPTIONS_TYPES)
1867
1349
  options = to_camel_case(options)
1868
1350
  data = self.request("/findSimilar", options)
@@ -1883,14 +1365,11 @@ class Exa:
1883
1365
  subpages=snake_result.get("subpages"),
1884
1366
  extras=snake_result.get("extras"),
1885
1367
  text=snake_result.get("text"),
1886
- highlights=snake_result.get("highlights"),
1887
- highlight_scores=snake_result.get("highlight_scores"),
1888
1368
  summary=snake_result.get("summary"),
1889
1369
  )
1890
1370
  )
1891
1371
  return SearchResponse(
1892
1372
  results,
1893
- data.get("autopromptString"),
1894
1373
  data.get("resolvedSearchType"),
1895
1374
  data.get("autoDate"),
1896
1375
  cost_dollars=cost_dollars,
@@ -1947,39 +1426,12 @@ class Exa:
1947
1426
  extras: Optional[ExtrasOptions] = None,
1948
1427
  ) -> SearchResponse[ResultWithText]: ...
1949
1428
 
1950
- @overload
1951
- def find_similar_and_contents(
1952
- self,
1953
- url: str,
1954
- *,
1955
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1956
- num_results: Optional[int] = None,
1957
- include_domains: Optional[List[str]] = None,
1958
- exclude_domains: Optional[List[str]] = None,
1959
- start_crawl_date: Optional[str] = None,
1960
- end_crawl_date: Optional[str] = None,
1961
- start_published_date: Optional[str] = None,
1962
- end_published_date: Optional[str] = None,
1963
- include_text: Optional[List[str]] = None,
1964
- exclude_text: Optional[List[str]] = None,
1965
- exclude_source_domain: Optional[bool] = None,
1966
- category: Optional[str] = None,
1967
- flags: Optional[List[str]] = None,
1968
- subpages: Optional[int] = None,
1969
- subpage_target: Optional[Union[str, List[str]]] = None,
1970
- livecrawl_timeout: Optional[int] = None,
1971
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1972
- filter_empty_results: Optional[bool] = None,
1973
- extras: Optional[ExtrasOptions] = None,
1974
- ) -> SearchResponse[ResultWithHighlights]: ...
1975
-
1976
1429
  @overload
1977
1430
  def find_similar_and_contents(
1978
1431
  self,
1979
1432
  url: str,
1980
1433
  *,
1981
1434
  text: Union[TextContentsOptions, Literal[True]],
1982
- highlights: Union[HighlightsContentsOptions, Literal[True]],
1983
1435
  num_results: Optional[int] = None,
1984
1436
  include_domains: Optional[List[str]] = None,
1985
1437
  exclude_domains: Optional[List[str]] = None,
@@ -1998,7 +1450,7 @@ class Exa:
1998
1450
  subpages: Optional[int] = None,
1999
1451
  subpage_target: Optional[Union[str, List[str]]] = None,
2000
1452
  extras: Optional[ExtrasOptions] = None,
2001
- ) -> SearchResponse[ResultWithTextAndHighlights]: ...
1453
+ ) -> SearchResponse[ResultWithText]: ...
2002
1454
 
2003
1455
  @overload
2004
1456
  def find_similar_and_contents(
@@ -2053,73 +1505,23 @@ class Exa:
2053
1505
  extras: Optional[ExtrasOptions] = None,
2054
1506
  ) -> SearchResponse[ResultWithTextAndSummary]: ...
2055
1507
 
2056
- @overload
2057
- def find_similar_and_contents(
2058
- self,
2059
- url: str,
2060
- *,
2061
- highlights: Union[HighlightsContentsOptions, Literal[True]],
2062
- summary: Union[SummaryContentsOptions, Literal[True]],
2063
- num_results: Optional[int] = None,
2064
- include_domains: Optional[List[str]] = None,
2065
- exclude_domains: Optional[List[str]] = None,
2066
- start_crawl_date: Optional[str] = None,
2067
- end_crawl_date: Optional[str] = None,
2068
- start_published_date: Optional[str] = None,
2069
- end_published_date: Optional[str] = None,
2070
- include_text: Optional[List[str]] = None,
2071
- exclude_text: Optional[List[str]] = None,
2072
- exclude_source_domain: Optional[bool] = None,
2073
- category: Optional[str] = None,
2074
- flags: Optional[List[str]] = None,
2075
- subpages: Optional[int] = None,
2076
- subpage_target: Optional[Union[str, List[str]]] = None,
2077
- livecrawl_timeout: Optional[int] = None,
2078
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
2079
- filter_empty_results: Optional[bool] = None,
2080
- extras: Optional[ExtrasOptions] = None,
2081
- ) -> SearchResponse[ResultWithHighlightsAndSummary]: ...
1508
+ def find_similar_and_contents(self, url: str, **kwargs):
1509
+ """
1510
+ DEPRECATED: Use find_similar() instead. The find_similar() method now returns text contents by default.
2082
1511
 
2083
- @overload
2084
- def find_similar_and_contents(
2085
- self,
2086
- url: str,
2087
- *,
2088
- text: Union[TextContentsOptions, Literal[True]],
2089
- highlights: Union[HighlightsContentsOptions, Literal[True]],
2090
- summary: Union[SummaryContentsOptions, Literal[True]],
2091
- num_results: Optional[int] = None,
2092
- include_domains: Optional[List[str]] = None,
2093
- exclude_domains: Optional[List[str]] = None,
2094
- start_crawl_date: Optional[str] = None,
2095
- end_crawl_date: Optional[str] = None,
2096
- start_published_date: Optional[str] = None,
2097
- end_published_date: Optional[str] = None,
2098
- include_text: Optional[List[str]] = None,
2099
- exclude_text: Optional[List[str]] = None,
2100
- exclude_source_domain: Optional[bool] = None,
2101
- category: Optional[str] = None,
2102
- flags: Optional[List[str]] = None,
2103
- livecrawl_timeout: Optional[int] = None,
2104
- livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
2105
- filter_empty_results: Optional[bool] = None,
2106
- subpages: Optional[int] = None,
2107
- subpage_target: Optional[Union[str, List[str]]] = None,
2108
- extras: Optional[ExtrasOptions] = None,
2109
- ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...
1512
+ Migration:
1513
+ - find_similar_and_contents(url) → find_similar(url)
1514
+ - find_similar_and_contents(url, text=True) → find_similar(url, contents={"text": True})
1515
+ - find_similar_and_contents(url, summary=True) → find_similar(url, contents={"summary": True})
1516
+ """
2110
1517
 
2111
- def find_similar_and_contents(self, url: str, **kwargs):
2112
1518
  options = {"url": url}
2113
1519
  for k, v in kwargs.items():
2114
1520
  if v is not None:
2115
1521
  options[k] = v
2116
- # Default to text if none specified
2117
- if (
2118
- "text" not in options
2119
- and "highlights" not in options
2120
- and "summary" not in options
2121
- ):
2122
- options["text"] = True
1522
+ # Default to text with max characters if none specified
1523
+ if "text" not in options and "summary" not in options:
1524
+ options["text"] = {"max_characters": DEFAULT_MAX_CHARACTERS}
2123
1525
 
2124
1526
  merged_options = {}
2125
1527
  merged_options.update(FIND_SIMILAR_OPTIONS_TYPES)
@@ -2138,7 +1540,6 @@ class Exa:
2138
1540
  options,
2139
1541
  [
2140
1542
  "text",
2141
- "highlights",
2142
1543
  "summary",
2143
1544
  "context",
2144
1545
  "subpages",
@@ -2168,14 +1569,11 @@ class Exa:
2168
1569
  subpages=snake_result.get("subpages"),
2169
1570
  extras=snake_result.get("extras"),
2170
1571
  text=snake_result.get("text"),
2171
- highlights=snake_result.get("highlights"),
2172
- highlight_scores=snake_result.get("highlight_scores"),
2173
1572
  summary=snake_result.get("summary"),
2174
1573
  )
2175
1574
  )
2176
1575
  return SearchResponse(
2177
1576
  results,
2178
- data.get("autopromptString"),
2179
1577
  data.get("resolvedSearchType"),
2180
1578
  data.get("autoDate"),
2181
1579
  context=data.get("context"),
@@ -2205,7 +1603,6 @@ class Exa:
2205
1603
  model: Union[str, ChatModel],
2206
1604
  # Exa args
2207
1605
  use_exa: Optional[Literal["required", "none", "auto"]] = "auto",
2208
- highlights: Union[HighlightsContentsOptions, Literal[True], None] = None,
2209
1606
  num_results: Optional[int] = 3,
2210
1607
  include_domains: Optional[List[str]] = None,
2211
1608
  exclude_domains: Optional[List[str]] = None,
@@ -2215,7 +1612,6 @@ class Exa:
2215
1612
  end_published_date: Optional[str] = None,
2216
1613
  include_text: Optional[List[str]] = None,
2217
1614
  exclude_text: Optional[List[str]] = None,
2218
- use_autoprompt: Optional[bool] = True,
2219
1615
  type: Optional[str] = None,
2220
1616
  category: Optional[str] = None,
2221
1617
  result_max_len: int = 2048,
@@ -2227,14 +1623,12 @@ class Exa:
2227
1623
  "num_results": num_results,
2228
1624
  "include_domains": include_domains,
2229
1625
  "exclude_domains": exclude_domains,
2230
- "highlights": highlights,
2231
1626
  "start_crawl_date": start_crawl_date,
2232
1627
  "end_crawl_date": end_crawl_date,
2233
1628
  "start_published_date": start_published_date,
2234
1629
  "end_published_date": end_published_date,
2235
1630
  "include_text": include_text,
2236
1631
  "exclude_text": exclude_text,
2237
- "use_autoprompt": use_autoprompt,
2238
1632
  "type": type,
2239
1633
  "category": category,
2240
1634
  "flags": flags,
@@ -2301,14 +1695,12 @@ class Exa:
2301
1695
  num_results=exa_kwargs.get("num_results"),
2302
1696
  include_domains=exa_kwargs.get("include_domains"),
2303
1697
  exclude_domains=exa_kwargs.get("exclude_domains"),
2304
- highlights=exa_kwargs.get("highlights"),
2305
1698
  start_crawl_date=exa_kwargs.get("start_crawl_date"),
2306
1699
  end_crawl_date=exa_kwargs.get("end_crawl_date"),
2307
1700
  start_published_date=exa_kwargs.get("start_published_date"),
2308
1701
  end_published_date=exa_kwargs.get("end_published_date"),
2309
1702
  include_text=exa_kwargs.get("include_text"),
2310
1703
  exclude_text=exa_kwargs.get("exclude_text"),
2311
- use_autoprompt=exa_kwargs.get("use_autoprompt"),
2312
1704
  type=exa_kwargs.get("type"),
2313
1705
  category=exa_kwargs.get("category"),
2314
1706
  flags=exa_kwargs.get("flags"),
@@ -2512,6 +1904,7 @@ class AsyncExa(Exa):
2512
1904
  self,
2513
1905
  query: str,
2514
1906
  *,
1907
+ contents: Optional[Union[Dict, bool]] = None,
2515
1908
  num_results: Optional[int] = None,
2516
1909
  include_domains: Optional[List[str]] = None,
2517
1910
  exclude_domains: Optional[List[str]] = None,
@@ -2521,17 +1914,20 @@ class AsyncExa(Exa):
2521
1914
  end_published_date: Optional[str] = None,
2522
1915
  include_text: Optional[List[str]] = None,
2523
1916
  exclude_text: Optional[List[str]] = None,
2524
- use_autoprompt: Optional[bool] = None,
2525
1917
  type: Optional[str] = None,
2526
1918
  category: Optional[str] = None,
2527
1919
  flags: Optional[List[str]] = None,
2528
1920
  moderation: Optional[bool] = None,
2529
1921
  user_location: Optional[str] = None,
2530
- ) -> SearchResponse[_Result]:
1922
+ ) -> SearchResponse[Result]:
2531
1923
  """Perform a search with a prompt-engineered query to retrieve relevant results.
2532
1924
 
1925
+ By default, returns text contents with 10,000 max characters. Use contents=False to opt-out.
1926
+
2533
1927
  Args:
2534
1928
  query (str): The query string.
1929
+ contents (dict | bool, optional): Options for retrieving page contents.
1930
+ Defaults to {"text": {"maxCharacters": 10000}}. Use False to disable contents.
2535
1931
  num_results (int, optional): Number of search results to return (default 10).
2536
1932
  include_domains (List[str], optional): Domains to include in the search.
2537
1933
  exclude_domains (List[str], optional): Domains to exclude from the search.
@@ -2541,7 +1937,6 @@ class AsyncExa(Exa):
2541
1937
  end_published_date (str, optional): Only links published before this date.
2542
1938
  include_text (List[str], optional): Strings that must appear in the page text.
2543
1939
  exclude_text (List[str], optional): Strings that must not appear in the page text.
2544
- use_autoprompt (bool, optional): Convert query to Exa (default False).
2545
1940
  type (str, optional): 'keyword', 'neural', 'hybrid', 'fast', 'deep', or 'auto' (default 'auto').
2546
1941
  category (str, optional): e.g. 'company'
2547
1942
  flags (List[str], optional): Experimental flags for Exa usage.
@@ -2552,6 +1947,18 @@ class AsyncExa(Exa):
2552
1947
  SearchResponse: The response containing search results, etc.
2553
1948
  """
2554
1949
  options = {k: v for k, v in locals().items() if k != "self" and v is not None}
1950
+
1951
+ # Handle contents parameter with default behavior
1952
+ if contents is False:
1953
+ # Explicitly no contents - remove from options
1954
+ options.pop("contents", None)
1955
+ elif contents is None and "contents" not in options:
1956
+ # No contents specified - add default text with 10,000 max characters
1957
+ options["contents"] = {"text": {"max_characters": DEFAULT_MAX_CHARACTERS}}
1958
+ elif contents is not None:
1959
+ # User provided contents - use as-is
1960
+ options["contents"] = contents
1961
+
2555
1962
  validate_search_options(options, SEARCH_OPTIONS_TYPES)
2556
1963
  options = to_camel_case(options)
2557
1964
  data = await self.async_request("/search", options)
@@ -2572,32 +1979,37 @@ class AsyncExa(Exa):
2572
1979
  subpages=snake_result.get("subpages"),
2573
1980
  extras=snake_result.get("extras"),
2574
1981
  text=snake_result.get("text"),
2575
- highlights=snake_result.get("highlights"),
2576
- highlight_scores=snake_result.get("highlight_scores"),
2577
1982
  summary=snake_result.get("summary"),
2578
1983
  )
2579
1984
  )
2580
1985
  return SearchResponse(
2581
1986
  results,
2582
- data["autopromptString"] if "autopromptString" in data else None,
2583
- data["resolvedSearchType"] if "resolvedSearchType" in data else None,
2584
- data["autoDate"] if "autoDate" in data else None,
1987
+ data.get("resolvedSearchType"),
1988
+ data.get("autoDate"),
2585
1989
  cost_dollars=cost_dollars,
2586
1990
  )
2587
1991
 
2588
1992
  async def search_and_contents(self, query: str, **kwargs):
1993
+ """
1994
+ DEPRECATED: Use search() instead. The search() method now returns text contents by default.
1995
+
1996
+ Migration:
1997
+ - search_and_contents(query) → search(query)
1998
+ - search_and_contents(query, text=True) → search(query, contents={"text": True})
1999
+ - search_and_contents(query, summary=True) → search(query, contents={"summary": True})
2000
+ """
2001
+
2589
2002
  options = {"query": query}
2590
2003
  for k, v in kwargs.items():
2591
2004
  if v is not None:
2592
2005
  options[k] = v
2593
- # If user didn't ask for any particular content, default to text
2006
+ # If user didn't ask for any particular content, default to text with max characters
2594
2007
  if (
2595
2008
  "text" not in options
2596
- and "highlights" not in options
2597
2009
  and "summary" not in options
2598
2010
  and "extras" not in options
2599
2011
  ):
2600
- options["text"] = True
2012
+ options["text"] = {"max_characters": DEFAULT_MAX_CHARACTERS}
2601
2013
 
2602
2014
  merged_options = {}
2603
2015
  merged_options.update(SEARCH_OPTIONS_TYPES)
@@ -2616,7 +2028,6 @@ class AsyncExa(Exa):
2616
2028
  options,
2617
2029
  [
2618
2030
  "text",
2619
- "highlights",
2620
2031
  "summary",
2621
2032
  "context",
2622
2033
  "subpages",
@@ -2646,21 +2057,25 @@ class AsyncExa(Exa):
2646
2057
  subpages=snake_result.get("subpages"),
2647
2058
  extras=snake_result.get("extras"),
2648
2059
  text=snake_result.get("text"),
2649
- highlights=snake_result.get("highlights"),
2650
- highlight_scores=snake_result.get("highlight_scores"),
2651
2060
  summary=snake_result.get("summary"),
2652
2061
  )
2653
2062
  )
2654
2063
  return SearchResponse(
2655
2064
  results,
2656
- data["autopromptString"] if "autopromptString" in data else None,
2657
- data["resolvedSearchType"] if "resolvedSearchType" in data else None,
2658
- data["autoDate"] if "autoDate" in data else None,
2065
+ data.get("resolvedSearchType"),
2066
+ data.get("autoDate"),
2659
2067
  context=data.get("context"),
2660
2068
  cost_dollars=cost_dollars,
2661
2069
  )
2662
2070
 
2663
2071
  async def get_contents(self, urls: Union[str, List[str], List[_Result]], **kwargs):
2072
+ # Normalize urls to always be a list
2073
+ if isinstance(urls, str):
2074
+ urls = [urls]
2075
+ elif isinstance(urls, list) and len(urls) > 0 and isinstance(urls[0], _Result):
2076
+ # Extract URLs from Result objects
2077
+ urls = [r.url for r in urls]
2078
+
2664
2079
  options = {"urls": urls}
2665
2080
  for k, v in kwargs.items():
2666
2081
  if k != "self" and v is not None:
@@ -2668,11 +2083,10 @@ class AsyncExa(Exa):
2668
2083
 
2669
2084
  if (
2670
2085
  "text" not in options
2671
- and "highlights" not in options
2672
2086
  and "summary" not in options
2673
2087
  and "extras" not in options
2674
2088
  ):
2675
- options["text"] = True
2089
+ options["text"] = {"max_characters": DEFAULT_MAX_CHARACTERS}
2676
2090
 
2677
2091
  merged_options = {}
2678
2092
  merged_options.update(CONTENTS_OPTIONS_TYPES)
@@ -2713,14 +2127,11 @@ class AsyncExa(Exa):
2713
2127
  subpages=snake_result.get("subpages"),
2714
2128
  extras=snake_result.get("extras"),
2715
2129
  text=snake_result.get("text"),
2716
- highlights=snake_result.get("highlights"),
2717
- highlight_scores=snake_result.get("highlight_scores"),
2718
2130
  summary=snake_result.get("summary"),
2719
2131
  )
2720
2132
  )
2721
2133
  return SearchResponse(
2722
2134
  results,
2723
- data.get("autopromptString"),
2724
2135
  data.get("resolvedSearchType"),
2725
2136
  data.get("autoDate"),
2726
2137
  context=data.get("context"),
@@ -2732,6 +2143,7 @@ class AsyncExa(Exa):
2732
2143
  self,
2733
2144
  url: str,
2734
2145
  *,
2146
+ contents: Optional[Union[Dict, bool]] = None,
2735
2147
  num_results: Optional[int] = None,
2736
2148
  include_domains: Optional[List[str]] = None,
2737
2149
  exclude_domains: Optional[List[str]] = None,
@@ -2744,11 +2156,15 @@ class AsyncExa(Exa):
2744
2156
  exclude_source_domain: Optional[bool] = None,
2745
2157
  category: Optional[str] = None,
2746
2158
  flags: Optional[List[str]] = None,
2747
- ) -> SearchResponse[_Result]:
2159
+ ) -> SearchResponse[Result]:
2748
2160
  """Finds similar pages to a given URL, potentially with domain filters and date filters.
2749
2161
 
2162
+ By default, returns text contents with 10,000 max characters. Use contents=False to opt-out.
2163
+
2750
2164
  Args:
2751
2165
  url (str): The URL to find similar pages for.
2166
+ contents (dict | bool, optional): Options for retrieving page contents.
2167
+ Defaults to {"text": {"maxCharacters": 10000}}. Use False to disable contents.
2752
2168
  num_results (int, optional): Number of results to return. Default is None (server default).
2753
2169
  include_domains (List[str], optional): Domains to include in the search.
2754
2170
  exclude_domains (List[str], optional): Domains to exclude from the search.
@@ -2763,9 +2179,21 @@ class AsyncExa(Exa):
2763
2179
  flags (List[str], optional): Experimental flags.
2764
2180
 
2765
2181
  Returns:
2766
- SearchResponse[_Result]
2182
+ SearchResponse[Result]
2767
2183
  """
2768
2184
  options = {k: v for k, v in locals().items() if k != "self" and v is not None}
2185
+
2186
+ # Handle contents parameter with default behavior
2187
+ if contents is False:
2188
+ # Explicitly no contents - remove from options
2189
+ options.pop("contents", None)
2190
+ elif contents is None and "contents" not in options:
2191
+ # No contents specified - add default text with 10,000 max characters
2192
+ options["contents"] = {"text": {"max_characters": DEFAULT_MAX_CHARACTERS}}
2193
+ elif contents is not None:
2194
+ # User provided contents - use as-is
2195
+ options["contents"] = contents
2196
+
2769
2197
  validate_search_options(options, FIND_SIMILAR_OPTIONS_TYPES)
2770
2198
  options = to_camel_case(options)
2771
2199
  data = await self.async_request("/findSimilar", options)
@@ -2786,31 +2214,32 @@ class AsyncExa(Exa):
2786
2214
  subpages=snake_result.get("subpages"),
2787
2215
  extras=snake_result.get("extras"),
2788
2216
  text=snake_result.get("text"),
2789
- highlights=snake_result.get("highlights"),
2790
- highlight_scores=snake_result.get("highlight_scores"),
2791
2217
  summary=snake_result.get("summary"),
2792
2218
  )
2793
2219
  )
2794
2220
  return SearchResponse(
2795
2221
  results,
2796
- data.get("autopromptString"),
2797
2222
  data.get("resolvedSearchType"),
2798
2223
  data.get("autoDate"),
2799
2224
  cost_dollars=cost_dollars,
2800
2225
  )
2801
2226
 
2802
2227
  async def find_similar_and_contents(self, url: str, **kwargs):
2228
+ """
2229
+ DEPRECATED: Use find_similar() instead. The find_similar() method now returns text contents by default.
2230
+
2231
+ Migration:
2232
+ - find_similar_and_contents(url) → find_similar(url)
2233
+ - find_similar_and_contents(url, text=True) → find_similar(url, contents={"text": True})
2234
+ - find_similar_and_contents(url, summary=True) → find_similar(url, contents={"summary": True})
2235
+ """
2803
2236
  options = {"url": url}
2804
2237
  for k, v in kwargs.items():
2805
2238
  if v is not None:
2806
2239
  options[k] = v
2807
- # Default to text if none specified
2808
- if (
2809
- "text" not in options
2810
- and "highlights" not in options
2811
- and "summary" not in options
2812
- ):
2813
- options["text"] = True
2240
+ # Default to text with max characters if none specified
2241
+ if "text" not in options and "summary" not in options:
2242
+ options["text"] = {"max_characters": DEFAULT_MAX_CHARACTERS}
2814
2243
 
2815
2244
  merged_options = {}
2816
2245
  merged_options.update(FIND_SIMILAR_OPTIONS_TYPES)
@@ -2829,7 +2258,6 @@ class AsyncExa(Exa):
2829
2258
  options,
2830
2259
  [
2831
2260
  "text",
2832
- "highlights",
2833
2261
  "summary",
2834
2262
  "context",
2835
2263
  "subpages",
@@ -2859,14 +2287,11 @@ class AsyncExa(Exa):
2859
2287
  subpages=snake_result.get("subpages"),
2860
2288
  extras=snake_result.get("extras"),
2861
2289
  text=snake_result.get("text"),
2862
- highlights=snake_result.get("highlights"),
2863
- highlight_scores=snake_result.get("highlight_scores"),
2864
2290
  summary=snake_result.get("summary"),
2865
2291
  )
2866
2292
  )
2867
2293
  return SearchResponse(
2868
2294
  results,
2869
- data.get("autopromptString"),
2870
2295
  data.get("resolvedSearchType"),
2871
2296
  data.get("autoDate"),
2872
2297
  context=data.get("context"),