exa-py 1.8.5__tar.gz → 1.8.6__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exa-py
3
- Version: 1.8.5
3
+ Version: 1.8.6
4
4
  Summary: Python SDK for Exa API.
5
5
  Author: Exa AI
6
6
  Author-email: hello@exa.ai
@@ -91,15 +91,16 @@ exa = Exa(api_key="your-api-key")
91
91
  # basic answer
92
92
  response = exa.answer("This is a query to answer a question")
93
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)
94
+ # answer with full text
95
+ response = exa.answer("This is a query to answer a question", text=True)
96
96
 
97
97
  # answer with streaming
98
- response = exa.answer("This is a query to answer with streaming:", stream=True)
98
+ response = exa.stream_answer("This is a query to answer:")
99
99
 
100
- # Print each chunk as it arrives when answer streaming is enabled
100
+ # Print each chunk as it arrives when using the stream_answer method
101
101
  for chunk in response:
102
- print(chunk)
102
+ print(chunk, end='', flush=True)
103
+
103
104
  ```
104
105
 
105
106
 
@@ -73,14 +73,15 @@ exa = Exa(api_key="your-api-key")
73
73
  # basic answer
74
74
  response = exa.answer("This is a query to answer a question")
75
75
 
76
- # answer with expanded queries and full text
77
- response = exa.answer("This is a query to answer a question", expanded_queries_limit=3, include_text=True)
76
+ # answer with full text
77
+ response = exa.answer("This is a query to answer a question", text=True)
78
78
 
79
79
  # answer with streaming
80
- response = exa.answer("This is a query to answer with streaming:", stream=True)
80
+ response = exa.stream_answer("This is a query to answer:")
81
81
 
82
- # Print each chunk as it arrives when answer streaming is enabled
82
+ # Print each chunk as it arrives when using the stream_answer method
83
83
  for chunk in response:
84
- print(chunk)
84
+ print(chunk, end='', flush=True)
85
+
85
86
  ```
86
87
 
@@ -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
 
@@ -536,8 +537,34 @@ class AnswerResult:
536
537
  f"ID: {self.id}\n"
537
538
  f"Published Date: {self.published_date}\n"
538
539
  f"Author: {self.author}\n"
539
- f"Text: {self.text}\n"
540
+ f"Text: {self.text}\n\n"
540
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
541
568
 
542
569
 
543
570
  @dataclass
@@ -553,11 +580,59 @@ class AnswerResponse:
553
580
  sources: List[AnswerResult]
554
581
 
555
582
  def __str__(self):
556
- output = f"Answer: {self.answer}\n\nSources:\n"
557
- 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"
558
592
  return output
559
593
 
560
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
+
561
636
  T = TypeVar("T")
562
637
 
563
638
 
@@ -611,7 +686,7 @@ class Exa:
611
686
  self,
612
687
  api_key: Optional[str],
613
688
  base_url: str = "https://api.exa.ai",
614
- user_agent: str = "exa-py 1.8.5",
689
+ user_agent: str = "exa-py 1.8.6",
615
690
  ):
616
691
  """Initialize the Exa client with the provided API key and optional base URL and user agent.
617
692
 
@@ -638,7 +713,7 @@ class Exa:
638
713
  data (dict): The JSON payload to send.
639
714
 
640
715
  Returns:
641
- 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.
642
717
  Otherwise, returns the JSON-decoded response as a dict.
643
718
 
644
719
  Raises:
@@ -646,9 +721,7 @@ class Exa:
646
721
  """
647
722
  if data.get("stream"):
648
723
  res = requests.post(self.base_url + endpoint, json=data, headers=self.headers, stream=True)
649
- if res.status_code != 200:
650
- raise ValueError(f"Request failed with status code {res.status_code}: {res.text}")
651
- return (line.decode("utf-8") for line in res.iter_lines() if line)
724
+ return res
652
725
 
653
726
  res = requests.post(self.base_url + endpoint, json=data, headers=self.headers)
654
727
  if res.status_code != 200:
@@ -730,6 +803,7 @@ class Exa:
730
803
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
731
804
  filter_empty_results: Optional[bool] = None,
732
805
  subpages: Optional[int] = None,
806
+ subpage_target: Optional[Union[str, List[str]]] = None,
733
807
  extras: Optional[ExtrasOptions] = None,
734
808
  ) -> SearchResponse[ResultWithText]:
735
809
  ...
@@ -752,12 +826,11 @@ class Exa:
752
826
  use_autoprompt: Optional[bool] = None,
753
827
  type: Optional[str] = None,
754
828
  category: Optional[str] = None,
755
- flags: Optional[List[str]] = None,
756
- moderation: Optional[bool] = None,
757
829
  subpages: Optional[int] = None,
758
830
  livecrawl_timeout: Optional[int] = None,
759
831
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
760
832
  filter_empty_results: Optional[bool] = None,
833
+ subpage_target: Optional[Union[str, List[str]]] = None,
761
834
  extras: Optional[ExtrasOptions] = None,
762
835
  ) -> SearchResponse[ResultWithText]:
763
836
  ...
@@ -901,8 +974,6 @@ class Exa:
901
974
  category: Optional[str] = None,
902
975
  subpages: Optional[int] = None,
903
976
  subpage_target: Optional[Union[str, List[str]]] = None,
904
- flags: Optional[List[str]] = None,
905
- moderation: Optional[bool] = None,
906
977
  livecrawl_timeout: Optional[int] = None,
907
978
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
908
979
  filter_empty_results: Optional[bool] = None,
@@ -931,12 +1002,11 @@ class Exa:
931
1002
  type: Optional[str] = None,
932
1003
  category: Optional[str] = None,
933
1004
  flags: Optional[List[str]] = None,
934
- moderation: Optional[bool] = None,
935
1005
  livecrawl_timeout: Optional[int] = None,
936
1006
  livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
1007
+ filter_empty_results: Optional[bool] = None,
937
1008
  subpages: Optional[int] = None,
938
1009
  subpage_target: Optional[Union[str, List[str]]] = None,
939
- filter_empty_results: Optional[bool] = None,
940
1010
  extras: Optional[ExtrasOptions] = None,
941
1011
  ) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]:
942
1012
  ...
@@ -1584,33 +1654,36 @@ class Exa:
1584
1654
  self,
1585
1655
  query: str,
1586
1656
  *,
1587
- expanded_queries_limit: Optional[int] = 1,
1588
1657
  stream: Optional[bool] = False,
1589
- include_text: Optional[bool] = False,
1590
- ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1658
+ text: Optional[bool] = False,
1659
+ ) -> Union[AnswerResponse, StreamAnswerResponse]:
1591
1660
  ...
1592
1661
 
1593
1662
  def answer(
1594
1663
  self,
1595
1664
  query: str,
1596
1665
  *,
1597
- expanded_queries_limit: Optional[int] = 1,
1598
1666
  stream: Optional[bool] = False,
1599
- include_text: Optional[bool] = False,
1600
- ) -> Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1667
+ text: Optional[bool] = False,
1668
+ ) -> Union[AnswerResponse, StreamAnswerResponse]:
1601
1669
  """Generate an answer to a query using Exa's search and LLM capabilities.
1602
1670
 
1603
1671
  Args:
1604
1672
  query (str): The query to answer.
1605
- expanded_queries_limit (int, optional): Maximum number of query variations (0-4). Defaults to 1.
1606
- stream (bool, optional): Whether to stream the response. Defaults to False.
1607
- 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.
1608
1674
 
1609
1675
  Returns:
1610
- Union[AnswerResponse, Iterator[Union[str, List[AnswerResult]]]]:
1611
- - If stream=False, returns an AnswerResponse object containing the answer and sources.
1612
- - 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.
1613
1680
  """
1681
+ if stream:
1682
+ raise ValueError(
1683
+ "stream=True is not supported in `answer()`. "
1684
+ "Please use `stream_answer(...)` for streaming."
1685
+ )
1686
+
1614
1687
  options = {
1615
1688
  k: v
1616
1689
  for k, v in locals().items()
@@ -1619,10 +1692,34 @@ class Exa:
1619
1692
  options = to_camel_case(options)
1620
1693
  response = self.request("/answer", options)
1621
1694
 
1622
- if stream:
1623
- return response
1624
-
1625
1695
  return AnswerResponse(
1626
1696
  response["answer"],
1627
1697
  [AnswerResult(**to_snake_case(result)) for result in response["sources"]]
1628
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "exa-py"
3
- version = "1.8.5"
3
+ version = "1.8.6"
4
4
  description = "Python SDK for Exa API."
5
5
  authors = ["Exa AI <hello@exa.ai>"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes