exa-py 1.7.2__py3-none-any.whl → 1.8.3__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
@@ -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
 
@@ -100,13 +101,13 @@ SEARCH_OPTIONS_TYPES = {
100
101
  "exclude_domains": [list], # Domains to omit; exclusive with 'include_domains'.
101
102
  "start_crawl_date": [str], # Results after this crawl date. ISO 8601 format.
102
103
  "end_crawl_date": [str], # Results before this crawl date. ISO 8601 format.
103
- "start_published_date": [str], # Results after this publish date; excludes links with no date.
104
- "end_published_date": [str], # Results before this publish date; excludes links with no date.
104
+ "start_published_date": [str], # Results after this publish date; excludes links with no date. ISO 8601 format.
105
+ "end_published_date": [str], # Results before this publish date; excludes links with no date. ISO 8601 format.
105
106
  "include_text": [list], # Must be present in webpage text. (One string, up to 5 words)
106
107
  "exclude_text": [list], # Must not be present in webpage text. (One string, up to 5 words)
107
108
  "use_autoprompt": [bool], # Convert query to Exa. (Default: false)
108
- "type": [str], # 'keyword' or 'neural' (Default: neural).
109
- "category": [str], # e.g. 'company'
109
+ "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
110
+ "category": [str], # A data category to focus on: 'company', 'research paper', 'news', 'pdf', 'github', 'tweet', 'personal site', 'linkedin profile', 'financial report'
110
111
  "flags": [list], # Experimental flags array for Exa usage.
111
112
  }
112
113
 
@@ -130,7 +131,7 @@ FIND_SIMILAR_OPTIONS_TYPES = {
130
131
  LIVECRAWL_OPTIONS = Literal["always", "fallback", "never", "auto"]
131
132
 
132
133
  CONTENTS_OPTIONS_TYPES = {
133
- "ids": [list],
134
+ "urls": [list],
134
135
  "text": [dict, bool],
135
136
  "highlights": [dict, bool],
136
137
  "summary": [dict, bool],
@@ -216,7 +217,6 @@ class SummaryContentsOptions(TypedDict, total=False):
216
217
 
217
218
  query: str
218
219
 
219
-
220
220
  class ExtrasOptions(TypedDict, total=False):
221
221
  """A class representing additional extraction fields (e.g. links, images)"""
222
222
 
@@ -273,7 +273,8 @@ class _Result:
273
273
  f"Published Date: {self.published_date}\n"
274
274
  f"Author: {self.author}\n"
275
275
  f"Image: {self.image}\n"
276
- f"Extras {self.extras}\n"
276
+ f"Favicon: {self.favicon}\n"
277
+ f"Extras: {self.extras}\n"
277
278
  f"Subpages: {self.subpages}\n"
278
279
  )
279
280
 
@@ -494,6 +495,56 @@ class ResultWithTextAndHighlightsAndSummary(_Result):
494
495
  f"Summary: {self.summary}\n"
495
496
  )
496
497
 
498
+ @dataclass
499
+ class AnswerResult:
500
+ """A class representing a source result for an answer.
501
+
502
+ Attributes:
503
+ title (str): The title of the search result.
504
+ url (str): The URL of the search result.
505
+ id (str): The temporary ID for the document.
506
+ published_date (str, optional): An estimate of the creation date, from parsing HTML content.
507
+ author (str, optional): If available, the author of the content.
508
+ """
509
+
510
+ url: str
511
+ id: str
512
+ title: Optional[str] = None
513
+ published_date: Optional[str] = None
514
+ author: Optional[str] = None
515
+
516
+ def __init__(self, **kwargs):
517
+ self.url = kwargs['url']
518
+ self.id = kwargs['id']
519
+ self.title = kwargs.get('title')
520
+ self.published_date = kwargs.get('published_date')
521
+ self.author = kwargs.get('author')
522
+
523
+ def __str__(self):
524
+ return (
525
+ f"Title: {self.title}\n"
526
+ f"URL: {self.url}\n"
527
+ f"ID: {self.id}\n"
528
+ f"Published Date: {self.published_date}\n"
529
+ f"Author: {self.author}\n"
530
+ )
531
+
532
+ @dataclass
533
+ class AnswerResponse:
534
+ """A class representing the response for an answer operation.
535
+
536
+ Attributes:
537
+ answer (str): The generated answer.
538
+ sources (List[AnswerResult]): A list of sources used to generate the answer.
539
+ """
540
+
541
+ answer: str
542
+ sources: List[AnswerResult]
543
+
544
+ def __str__(self):
545
+ output = f"Answer: {self.answer}\n\nSources:\n"
546
+ output += "\n\n".join(str(source) for source in self.sources)
547
+ return output
497
548
 
498
549
  T = TypeVar("T")
499
550
 
@@ -548,7 +599,7 @@ class Exa:
548
599
  self,
549
600
  api_key: Optional[str],
550
601
  base_url: str = "https://api.exa.ai",
551
- user_agent: str = "exa-py 1.7.2",
602
+ user_agent: str = "exa-py 1.8.3",
552
603
  ):
553
604
  """Initialize the Exa client with the provided API key and optional base URL and user agent.
554
605
 
@@ -568,11 +619,15 @@ class Exa:
568
619
  self.headers = {"x-api-key": api_key, "User-Agent": user_agent}
569
620
 
570
621
  def request(self, endpoint: str, data):
622
+ if data.get("stream"):
623
+ res = requests.post(self.base_url + endpoint, json=data, headers=self.headers, stream=True)
624
+ if res.status_code != 200:
625
+ raise ValueError(f"Request failed with status code {res.status_code}: {res.text}")
626
+ return (line.decode('utf-8') for line in res.iter_lines() if line)
627
+
571
628
  res = requests.post(self.base_url + endpoint, json=data, headers=self.headers)
572
629
  if res.status_code != 200:
573
- raise ValueError(
574
- f"Request failed with status code {res.status_code}: {res.text}"
575
- )
630
+ raise ValueError(f"Request failed with status code {res.status_code}: {res.text}")
576
631
  return res.json()
577
632
 
578
633
  def search(
@@ -894,7 +949,7 @@ class Exa:
894
949
  @overload
895
950
  def get_contents(
896
951
  self,
897
- ids: Union[str, List[str], List[_Result]],
952
+ urls: Union[str, List[str], List[_Result]],
898
953
  livecrawl_timeout: Optional[int] = None,
899
954
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
900
955
  filter_empty_results: Optional[bool] = None,
@@ -908,7 +963,7 @@ class Exa:
908
963
  @overload
909
964
  def get_contents(
910
965
  self,
911
- ids: Union[str, List[str], List[_Result]],
966
+ urls: Union[str, List[str], List[_Result]],
912
967
  *,
913
968
  text: Union[TextContentsOptions, Literal[True]],
914
969
  livecrawl_timeout: Optional[int] = None,
@@ -924,7 +979,7 @@ class Exa:
924
979
  @overload
925
980
  def get_contents(
926
981
  self,
927
- ids: Union[str, List[str], List[_Result]],
982
+ urls: Union[str, List[str], List[_Result]],
928
983
  *,
929
984
  highlights: Union[HighlightsContentsOptions, Literal[True]],
930
985
  livecrawl_timeout: Optional[int] = None,
@@ -940,7 +995,7 @@ class Exa:
940
995
  @overload
941
996
  def get_contents(
942
997
  self,
943
- ids: Union[str, List[str], List[_Result]],
998
+ urls: Union[str, List[str], List[_Result]],
944
999
  *,
945
1000
  text: Union[TextContentsOptions, Literal[True]],
946
1001
  highlights: Union[HighlightsContentsOptions, Literal[True]],
@@ -957,7 +1012,7 @@ class Exa:
957
1012
  @overload
958
1013
  def get_contents(
959
1014
  self,
960
- ids: Union[str, List[str], List[_Result]],
1015
+ urls: Union[str, List[str], List[_Result]],
961
1016
  *,
962
1017
  summary: Union[SummaryContentsOptions, Literal[True]],
963
1018
  livecrawl_timeout: Optional[int] = None,
@@ -973,7 +1028,7 @@ class Exa:
973
1028
  @overload
974
1029
  def get_contents(
975
1030
  self,
976
- ids: Union[str, List[str], List[_Result]],
1031
+ urls: Union[str, List[str], List[_Result]],
977
1032
  *,
978
1033
  text: Union[TextContentsOptions, Literal[True]],
979
1034
  summary: Union[SummaryContentsOptions, Literal[True]],
@@ -990,7 +1045,7 @@ class Exa:
990
1045
  @overload
991
1046
  def get_contents(
992
1047
  self,
993
- ids: Union[str, List[str], List[_Result]],
1048
+ urls: Union[str, List[str], List[_Result]],
994
1049
  *,
995
1050
  highlights: Union[HighlightsContentsOptions, Literal[True]],
996
1051
  summary: Union[SummaryContentsOptions, Literal[True]],
@@ -1007,7 +1062,7 @@ class Exa:
1007
1062
  @overload
1008
1063
  def get_contents(
1009
1064
  self,
1010
- ids: Union[str, List[str], List[_Result]],
1065
+ urls: Union[str, List[str], List[_Result]],
1011
1066
  *,
1012
1067
  text: Union[TextContentsOptions, Literal[True]],
1013
1068
  highlights: Union[HighlightsContentsOptions, Literal[True]],
@@ -1021,16 +1076,13 @@ class Exa:
1021
1076
  flags: Optional[List[str]] = None,
1022
1077
  ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
1023
1078
  ...
1024
-
1025
- def get_contents(self, ids: Union[str, List[str], List[_Result]], **kwargs):
1026
- options = {k: v for k, v in {"ids": ids, **kwargs}.items() if v is not None}
1027
- # Default to 'text' if none of text/highlights/summary are specified
1028
- if (
1029
- "text" not in options
1030
- and "highlights" not in options
1031
- and "summary" not in options
1032
- and "extras" not in options
1033
- ):
1079
+ def get_contents(self, urls: Union[str, List[str], List[_Result]], **kwargs):
1080
+ options = {
1081
+ k: v
1082
+ for k, v in {"urls": urls, **kwargs}.items()
1083
+ if k != "self" and v is not None
1084
+ }
1085
+ if "text" not in options and "highlights" not in options and "summary" not in options and "extras" not in options:
1034
1086
  options["text"] = True
1035
1087
 
1036
1088
  validate_search_options(
@@ -1456,3 +1508,50 @@ class Exa:
1456
1508
  completion=completion, exa_result=exa_result
1457
1509
  )
1458
1510
  return exa_completion
1511
+
1512
+ @overload
1513
+ def answer(
1514
+ self,
1515
+ query: str,
1516
+ *,
1517
+ expanded_queries_limit: Optional[int] = 1,
1518
+ stream: Optional[bool] = False,
1519
+ include_text: Optional[bool] = False,
1520
+ ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1521
+ ...
1522
+
1523
+ def answer(
1524
+ self,
1525
+ query: str,
1526
+ *,
1527
+ expanded_queries_limit: Optional[int] = 1,
1528
+ stream: Optional[bool] = False,
1529
+ include_text: Optional[bool] = False,
1530
+ ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1531
+ """Generate an answer to a query using Exa's search and LLM capabilities.
1532
+
1533
+ Args:
1534
+ query (str): The query to answer.
1535
+ expanded_queries_limit (int, optional): Maximum number of query variations (0-4). Defaults to 1.
1536
+ stream (bool, optional): Whether to stream the response. Defaults to False.
1537
+ include_text (bool, optional): Whether to include full text in the results. Defaults to False.
1538
+
1539
+ Returns:
1540
+ Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]: Either an AnswerResponse object containing the answer and sources,
1541
+ or an iterator that yields either answer chunks or sources when streaming is enabled.
1542
+ """
1543
+ options = {
1544
+ k: v
1545
+ for k, v in locals().items()
1546
+ if k != "self" and v is not None
1547
+ }
1548
+ options = to_camel_case(options)
1549
+ response = self.request("/answer", options)
1550
+
1551
+ if stream:
1552
+ return response
1553
+
1554
+ return AnswerResponse(
1555
+ response["answer"],
1556
+ [AnswerResult(**to_snake_case(result)) for result in response["sources"]]
1557
+ )
@@ -1,23 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: exa-py
3
- Version: 1.7.2
3
+ Version: 1.8.3
4
4
  Summary: Python SDK for Exa API.
5
- Home-page: https://github.com/exa-labs/exa-py
6
- Author: Exa
5
+ Author: Exa AI
7
6
  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
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
13
9
  Classifier: Programming Language :: Python :: 3.9
14
10
  Classifier: Programming Language :: Python :: 3.10
15
11
  Classifier: Programming Language :: Python :: 3.11
16
12
  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)
21
18
 
22
19
  # Exa
23
20
 
@@ -81,14 +78,28 @@ exa = Exa(api_key="your-api-key")
81
78
  results = exa.find_similar_and_contents("https://example.com", text=True, highlights=True)
82
79
 
83
80
  # get text contents
84
- results = exa.get_contents(["ids"])
81
+ results = exa.get_contents(["urls"])
85
82
 
86
83
  # get highlights
87
- results = exa.get_contents(["ids"], highlights=True)
84
+ results = exa.get_contents(["urls"], highlights=True)
88
85
 
89
86
  # get contents with contents options
90
- results = exa.get_contents(["ids"],
87
+ results = exa.get_contents(["urls"],
91
88
  text={"include_html_tags": True, "max_characters": 1000},
92
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)
93
103
  ```
94
104
 
105
+
@@ -0,0 +1,7 @@
1
+ exa_py/__init__.py,sha256=1selemczpRm1y8V9cWNm90LARnU1jbtyp-Qpx3c7cTw,28
2
+ exa_py/api.py,sha256=1Bc9S8OMgGwtih1hXqhYzv54d2Sj04EunMuNoDuqqrg,57257
3
+ exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
5
+ exa_py-1.8.3.dist-info/METADATA,sha256=Tp7sUiNVUCyL37ZcfL7v0goOSHn0q35vg-qrYLYAG2I,3389
6
+ exa_py-1.8.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
7
+ exa_py-1.8.3.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
2
+ Generator: poetry-core 2.0.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,8 +0,0 @@
1
- exa_py/__init__.py,sha256=1selemczpRm1y8V9cWNm90LARnU1jbtyp-Qpx3c7cTw,28
2
- exa_py/api.py,sha256=ssd6BKnt6cTmj7sXRFZRL5UljxV8oD545m91_YeEH7w,53349
3
- exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
5
- exa_py-1.7.2.dist-info/METADATA,sha256=sL65wDOkdMBBlOhxuZpD4UxxXqab0myIWc6qnYvcjsY,3032
6
- exa_py-1.7.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
7
- exa_py-1.7.2.dist-info/top_level.txt,sha256=Mfkmscdw9HWR1PtVhU1gAiVo6DHu_tyiVdb89gfZBVI,7
8
- exa_py-1.7.2.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- exa_py