exa-py 1.8.4__py3-none-any.whl → 1.8.6__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
@@ -17,8 +17,10 @@ from typing import (
17
17
  Literal,
18
18
  get_origin,
19
19
  get_args,
20
+ Iterator,
20
21
  )
21
22
  from typing_extensions import TypedDict
23
+ import json
22
24
 
23
25
  from openai import OpenAI
24
26
  from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam
@@ -30,7 +32,6 @@ from exa_py.utils import (
30
32
  maybe_get_query,
31
33
  )
32
34
  import os
33
- from typing import Iterator
34
35
 
35
36
  is_beta = os.getenv("IS_BETA") == "True"
36
37
 
@@ -517,15 +518,17 @@ class AnswerResult:
517
518
  url: str
518
519
  id: str
519
520
  title: Optional[str] = None
520
- published_date: Optional[str] = None
521
521
  author: Optional[str] = None
522
+ published_date: Optional[str] = None
523
+ text: Optional[str] = None
522
524
 
523
525
  def __init__(self, **kwargs):
524
- self.url = kwargs["url"]
525
- self.id = kwargs["id"]
526
- self.title = kwargs.get("title")
527
- self.published_date = kwargs.get("published_date")
528
- self.author = kwargs.get("author")
526
+ self.url = kwargs['url']
527
+ self.id = kwargs['id']
528
+ self.title = kwargs.get('title')
529
+ self.author = kwargs.get('author')
530
+ self.published_date = kwargs.get('published_date')
531
+ self.text = kwargs.get('text')
529
532
 
530
533
  def __str__(self):
531
534
  return (
@@ -534,7 +537,34 @@ class AnswerResult:
534
537
  f"ID: {self.id}\n"
535
538
  f"Published Date: {self.published_date}\n"
536
539
  f"Author: {self.author}\n"
540
+ f"Text: {self.text}\n\n"
537
541
  )
542
+
543
+ @dataclass
544
+ class StreamChunk:
545
+ """A class representing a single chunk of streaming data.
546
+
547
+ Attributes:
548
+ content (Optional[str]): The partial text content of the answer
549
+ sources (Optional[List[AnswerResult]]): List of sources if provided in this chunk
550
+ """
551
+ content: Optional[str] = None
552
+ sources: Optional[List[AnswerResult]] = None
553
+
554
+ def has_data(self) -> bool:
555
+ """Check if this chunk contains any data."""
556
+ return self.content is not None or self.sources is not None
557
+
558
+ def __str__(self) -> str:
559
+ """Format the chunk data as a string."""
560
+ output = ""
561
+ if self.content:
562
+ output += self.content
563
+ if self.sources:
564
+ output += "\nSources:"
565
+ for source in self.sources:
566
+ output += f"\n{source}"
567
+ return output
538
568
 
539
569
 
540
570
  @dataclass
@@ -550,11 +580,59 @@ class AnswerResponse:
550
580
  sources: List[AnswerResult]
551
581
 
552
582
  def __str__(self):
553
- output = f"Answer: {self.answer}\n\nSources:\n"
554
- output += "\n\n".join(str(source) for source in self.sources)
583
+ output = f"Answer: {self.answer}\n\nSources:"
584
+ for source in self.sources:
585
+ output += f"\nTitle: {source.title}"
586
+ output += f"\nURL: {source.url}"
587
+ output += f"\nPublished: {source.published_date}"
588
+ output += f"\nAuthor: {source.author}"
589
+ if source.text:
590
+ output += f"\nText: {source.text}"
591
+ output += "\n"
555
592
  return output
556
593
 
557
594
 
595
+ class StreamAnswerResponse:
596
+ """A class representing a streaming answer response."""
597
+ def __init__(self, raw_response: requests.Response):
598
+ self._raw_response = raw_response
599
+ self._ensure_ok_status()
600
+
601
+ def _ensure_ok_status(self):
602
+ if self._raw_response.status_code != 200:
603
+ raise ValueError(
604
+ f"Request failed with status code {self._raw_response.status_code}: {self._raw_response.text}"
605
+ )
606
+
607
+ def __iter__(self) -> Iterator[StreamChunk]:
608
+ for line in self._raw_response.iter_lines():
609
+ if not line:
610
+ continue
611
+ decoded_line = line.decode("utf-8").removeprefix("data: ")
612
+ try:
613
+ chunk = json.loads(decoded_line)
614
+ except json.JSONDecodeError:
615
+ continue
616
+
617
+ content = None
618
+ sources = None
619
+
620
+ if "choices" in chunk and chunk["choices"]:
621
+ if "delta" in chunk["choices"][0]:
622
+ content = chunk["choices"][0]["delta"].get("content")
623
+
624
+ if "sources" in chunk and chunk["sources"] and chunk["sources"] != "null":
625
+ sources = [AnswerResult(**to_snake_case(s)) for s in chunk["sources"]]
626
+
627
+ stream_chunk = StreamChunk(content=content, sources=sources)
628
+ if stream_chunk.has_data():
629
+ yield stream_chunk
630
+
631
+ def close(self) -> None:
632
+ """Close the underlying raw response to release the network socket."""
633
+ self._raw_response.close()
634
+
635
+
558
636
  T = TypeVar("T")
559
637
 
560
638
 
@@ -608,7 +686,7 @@ class Exa:
608
686
  self,
609
687
  api_key: Optional[str],
610
688
  base_url: str = "https://api.exa.ai",
611
- user_agent: str = "exa-py 1.8.4",
689
+ user_agent: str = "exa-py 1.8.6",
612
690
  ):
613
691
  """Initialize the Exa client with the provided API key and optional base URL and user agent.
614
692
 
@@ -635,7 +713,7 @@ class Exa:
635
713
  data (dict): The JSON payload to send.
636
714
 
637
715
  Returns:
638
- Union[dict, Iterator[str]]: If streaming, returns an iterator of strings (line-by-line).
716
+ Union[dict, requests.Response]: If streaming, returns the Response object.
639
717
  Otherwise, returns the JSON-decoded response as a dict.
640
718
 
641
719
  Raises:
@@ -643,9 +721,7 @@ class Exa:
643
721
  """
644
722
  if data.get("stream"):
645
723
  res = requests.post(self.base_url + endpoint, json=data, headers=self.headers, stream=True)
646
- if res.status_code != 200:
647
- raise ValueError(f"Request failed with status code {res.status_code}: {res.text}")
648
- return (line.decode("utf-8") for line in res.iter_lines() if line)
724
+ return res
649
725
 
650
726
  res = requests.post(self.base_url + endpoint, json=data, headers=self.headers)
651
727
  if res.status_code != 200:
@@ -727,6 +803,7 @@ class Exa:
727
803
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
728
804
  filter_empty_results: Optional[bool] = None,
729
805
  subpages: Optional[int] = None,
806
+ subpage_target: Optional[Union[str, List[str]]] = None,
730
807
  extras: Optional[ExtrasOptions] = None,
731
808
  ) -> SearchResponse[ResultWithText]:
732
809
  ...
@@ -749,12 +826,11 @@ class Exa:
749
826
  use_autoprompt: Optional[bool] = None,
750
827
  type: Optional[str] = None,
751
828
  category: Optional[str] = None,
752
- flags: Optional[List[str]] = None,
753
- moderation: Optional[bool] = None,
754
829
  subpages: Optional[int] = None,
755
830
  livecrawl_timeout: Optional[int] = None,
756
831
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
757
832
  filter_empty_results: Optional[bool] = None,
833
+ subpage_target: Optional[Union[str, List[str]]] = None,
758
834
  extras: Optional[ExtrasOptions] = None,
759
835
  ) -> SearchResponse[ResultWithText]:
760
836
  ...
@@ -898,8 +974,6 @@ class Exa:
898
974
  category: Optional[str] = None,
899
975
  subpages: Optional[int] = None,
900
976
  subpage_target: Optional[Union[str, List[str]]] = None,
901
- flags: Optional[List[str]] = None,
902
- moderation: Optional[bool] = None,
903
977
  livecrawl_timeout: Optional[int] = None,
904
978
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
905
979
  filter_empty_results: Optional[bool] = None,
@@ -928,12 +1002,11 @@ class Exa:
928
1002
  type: Optional[str] = None,
929
1003
  category: Optional[str] = None,
930
1004
  flags: Optional[List[str]] = None,
931
- moderation: Optional[bool] = None,
932
1005
  livecrawl_timeout: Optional[int] = None,
933
1006
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1007
+ filter_empty_results: Optional[bool] = None,
934
1008
  subpages: Optional[int] = None,
935
1009
  subpage_target: Optional[Union[str, List[str]]] = None,
936
- filter_empty_results: Optional[bool] = None,
937
1010
  extras: Optional[ExtrasOptions] = None,
938
1011
  ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
939
1012
  ...
@@ -1581,33 +1654,36 @@ class Exa:
1581
1654
  self,
1582
1655
  query: str,
1583
1656
  *,
1584
- expanded_queries_limit: Optional[int] = 1,
1585
1657
  stream: Optional[bool] = False,
1586
- include_text: Optional[bool] = False,
1587
- ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1658
+ text: Optional[bool] = False,
1659
+ ) -> Union[AnswerResponse, StreamAnswerResponse]:
1588
1660
  ...
1589
1661
 
1590
1662
  def answer(
1591
1663
  self,
1592
1664
  query: str,
1593
1665
  *,
1594
- expanded_queries_limit: Optional[int] = 1,
1595
1666
  stream: Optional[bool] = False,
1596
- include_text: Optional[bool] = False,
1597
- ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1667
+ text: Optional[bool] = False,
1668
+ ) -> Union[AnswerResponse, StreamAnswerResponse]:
1598
1669
  """Generate an answer to a query using Exa's search and LLM capabilities.
1599
1670
 
1600
1671
  Args:
1601
1672
  query (str): The query to answer.
1602
- expanded_queries_limit (int, optional): Maximum number of query variations (0-4). Defaults to 1.
1603
- stream (bool, optional): Whether to stream the response. Defaults to False.
1604
- include_text (bool, optional): Whether to include full text in the results. Defaults to False.
1673
+ text (bool, optional): Whether to include full text in the results. Defaults to False.
1605
1674
 
1606
1675
  Returns:
1607
- Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1608
- - If stream=False, returns an AnswerResponse object containing the answer and sources.
1609
- - If stream=True, returns an iterator that yields either answer chunks or sources.
1676
+ AnswerResponse: An object containing the answer and sources.
1677
+
1678
+ Raises:
1679
+ ValueError: If stream=True is provided. Use stream_answer() instead for streaming responses.
1610
1680
  """
1681
+ if stream:
1682
+ raise ValueError(
1683
+ "stream=True is not supported in `answer()`. "
1684
+ "Please use `stream_answer(...)` for streaming."
1685
+ )
1686
+
1611
1687
  options = {
1612
1688
  k: v
1613
1689
  for k, v in locals().items()
@@ -1616,10 +1692,34 @@ class Exa:
1616
1692
  options = to_camel_case(options)
1617
1693
  response = self.request("/answer", options)
1618
1694
 
1619
- if stream:
1620
- return response
1621
-
1622
1695
  return AnswerResponse(
1623
1696
  response["answer"],
1624
1697
  [AnswerResult(**to_snake_case(result)) for result in response["sources"]]
1625
1698
  )
1699
+
1700
+ def stream_answer(
1701
+ self,
1702
+ query: str,
1703
+ *,
1704
+ text: bool = False,
1705
+ ) -> StreamAnswerResponse:
1706
+ """Generate a streaming answer response.
1707
+
1708
+ Args:
1709
+ query (str): The query to answer.
1710
+ text (bool): Whether to include full text in the results. Defaults to False.
1711
+
1712
+ Returns:
1713
+ StreamAnswerResponse: An object that can be iterated over to retrieve (partial text, partial sources).
1714
+ Each iteration yields a tuple of (Optional[str], Optional[List[AnswerResult]]).
1715
+ """
1716
+ options = {
1717
+ k: v
1718
+ for k, v in locals().items()
1719
+ if k != "self" and v is not None
1720
+ }
1721
+ options = to_camel_case(options)
1722
+ options["stream"] = True
1723
+ raw_response = self.request("/answer", options)
1724
+ return StreamAnswerResponse(raw_response)
1725
+
@@ -1,23 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: exa-py
3
- Version: 1.8.4
3
+ Version: 1.8.6
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
 
@@ -94,14 +91,16 @@ exa = Exa(api_key="your-api-key")
94
91
  # basic answer
95
92
  response = exa.answer("This is a query to answer a question")
96
93
 
97
- # answer with expanded queries and full text
98
- response = exa.answer("This is a query to answer a question", expanded_queries_limit=3, include_text=True)
94
+ # answer with full text
95
+ response = exa.answer("This is a query to answer a question", text=True)
99
96
 
100
97
  # answer with streaming
101
- response = exa.answer("This is a query to answer with streaming:", stream=True)
98
+ response = exa.stream_answer("This is a query to answer:")
102
99
 
103
- # Print each chunk as it arrives when answer streaming is enabled
100
+ # Print each chunk as it arrives when using the stream_answer method
104
101
  for chunk in response:
105
- print(chunk)
102
+ print(chunk, end='', flush=True)
103
+
106
104
  ```
107
105
 
106
+
@@ -0,0 +1,7 @@
1
+ exa_py/__init__.py,sha256=1selemczpRm1y8V9cWNm90LARnU1jbtyp-Qpx3c7cTw,28
2
+ exa_py/api.py,sha256=MJbdvN1AAVS7GA9IUT56KF8U_R8AT-sIcvCVG0EY6YM,62530
3
+ exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
5
+ exa_py-1.8.6.dist-info/METADATA,sha256=jOlJfJYWMCEhoOGW1FY-bDjHLogR5soamiu94sPOuiw,3337
6
+ exa_py-1.8.6.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
7
+ exa_py-1.8.6.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=MiNGWxhy4ppEV5Buo5jqh0kJiPzTZ-DnZKQVQsIzOl4,59489
3
- exa_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- exa_py/utils.py,sha256=Rc1FJjoR9LQ7L_OJM91Sd1GNkbHjcLyEvJENhRix6gc,2405
5
- exa_py-1.8.4.dist-info/METADATA,sha256=zb1BWHXdwTnZJuhz7HCS0Cqka98BektdVyWw5E32eoE,3494
6
- exa_py-1.8.4.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
7
- exa_py-1.8.4.dist-info/top_level.txt,sha256=Mfkmscdw9HWR1PtVhU1gAiVo6DHu_tyiVdb89gfZBVI,7
8
- exa_py-1.8.4.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- exa_py