exa-py 1.7.3__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
 
@@ -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.3",
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(
@@ -1453,3 +1508,50 @@ class Exa:
1453
1508
  completion=completion, exa_result=exa_result
1454
1509
  )
1455
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exa-py
3
- Version: 1.7.3
3
+ Version: 1.8.3
4
4
  Summary: Python SDK for Exa API.
5
5
  Author: Exa AI
6
6
  Author-email: hello@exa.ai
@@ -87,6 +87,19 @@ exa = Exa(api_key="your-api-key")
87
87
  results = exa.get_contents(["urls"],
88
88
  text={"include_html_tags": True, "max_characters": 1000},
89
89
  highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
90
+
91
+ # 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)
90
103
  ```
91
104
 
92
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,7 +0,0 @@
1
- exa_py/__init__.py,sha256=1selemczpRm1y8V9cWNm90LARnU1jbtyp-Qpx3c7cTw,28
2
- exa_py/api.py,sha256=s2PzWTN_NSECTlJjEnaN22frTnoxXw0PdO2aalKUX2I,53610
3
- exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
5
- exa_py-1.7.3.dist-info/METADATA,sha256=tMUP_KFmo-S9zvX72MFY3qW8MIMMj5ZtYUldP_4fTt4,2930
6
- exa_py-1.7.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
7
- exa_py-1.7.3.dist-info/RECORD,,
File without changes