exa-py 1.8.4__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,19 +1,19 @@
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
18
 
19
19
  # Exa
@@ -91,14 +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
 
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
 
@@ -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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "exa-py"
3
- version = "1.8.3"
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"
exa_py-1.8.4/PKG-INFO DELETED
@@ -1,104 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: exa_py
3
- Version: 1.8.4
4
- Summary: Python SDK for Exa API.
5
- Home-page: https://github.com/exa-labs/exa-py
6
- Author: Exa
7
- 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
13
- Classifier: Programming Language :: Python :: 3.9
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Description-Content-Type: text/markdown
18
-
19
- # Exa
20
-
21
- Exa (formerly Metaphor) API in Python
22
-
23
- Note: This API is basically the same as `metaphor-python` but reflects new
24
- features associated with Metaphor's rename to Exa. New site is https://exa.ai
25
-
26
- ## Installation
27
-
28
- ```bash
29
- pip install exa_py
30
- ```
31
-
32
- ## Usage
33
-
34
- Import the package and initialize the Exa client with your API key:
35
-
36
- ```python
37
- from exa_py import Exa
38
-
39
- exa = Exa(api_key="your-api-key")
40
- ```
41
-
42
- ## Common requests
43
- ```python
44
-
45
- # basic search
46
- results = exa.search("This is a Exa query:")
47
-
48
- # autoprompted search
49
- results = exa.search("autopromptable query", use_autoprompt=True)
50
-
51
- # keyword search (non-neural)
52
- results = exa.search("Google-style query", type="keyword")
53
-
54
- # search with date filters
55
- results = exa.search("This is a Exa query:", start_published_date="2019-01-01", end_published_date="2019-01-31")
56
-
57
- # search with domain filters
58
- results = exa.search("This is a Exa query:", include_domains=["www.cnn.com", "www.nytimes.com"])
59
-
60
- # search and get text contents
61
- results = exa.search_and_contents("This is a Exa query:")
62
-
63
- # search and get highlights
64
- results = exa.search_and_contents("This is a Exa query:", highlights=True)
65
-
66
- # search and get contents with contents options
67
- results = exa.search_and_contents("This is a Exa query:",
68
- text={"include_html_tags": True, "max_characters": 1000},
69
- highlights={"highlights_per_url": 2, "num_sentences": 1, "query": "This is the highlight query:"})
70
-
71
- # find similar documents
72
- results = exa.find_similar("https://example.com")
73
-
74
- # find similar excluding source domain
75
- results = exa.find_similar("https://example.com", exclude_source_domain=True)
76
-
77
- # find similar with contents
78
- results = exa.find_similar_and_contents("https://example.com", text=True, highlights=True)
79
-
80
- # get text contents
81
- results = exa.get_contents(["urls"])
82
-
83
- # get highlights
84
- results = exa.get_contents(["urls"], highlights=True)
85
-
86
- # get contents with contents options
87
- results = exa.get_contents(["urls"],
88
- text={"include_html_tags": True, "max_characters": 1000},
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)
103
- ```
104
-
@@ -1,12 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- setup.py
4
- exa_py/__init__.py
5
- exa_py/api.py
6
- exa_py/py.typed
7
- exa_py/utils.py
8
- exa_py.egg-info/PKG-INFO
9
- exa_py.egg-info/SOURCES.txt
10
- exa_py.egg-info/dependency_links.txt
11
- exa_py.egg-info/requires.txt
12
- exa_py.egg-info/top_level.txt
@@ -1,3 +0,0 @@
1
- requests
2
- typing-extensions
3
- openai>=1.10.0
@@ -1 +0,0 @@
1
- exa_py
exa_py-1.8.4/setup.cfg DELETED
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
exa_py-1.8.4/setup.py DELETED
@@ -1,30 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- setup(
4
- name="exa_py",
5
- version="1.8.4",
6
- description="Python SDK for Exa API.",
7
- long_description_content_type="text/markdown",
8
- long_description=open("README.md").read(),
9
- author="Exa",
10
- author_email="hello@exa.ai",
11
- package_data={"exa_py": ["py.typed"]},
12
- url="https://github.com/exa-labs/exa-py",
13
- packages=find_packages(),
14
- install_requires=[
15
- "requests",
16
- "typing-extensions",
17
- "openai>=1.10.0"
18
- ],
19
- classifiers=[
20
- "Development Status :: 5 - Production/Stable",
21
- "Intended Audience :: Developers",
22
- "License :: OSI Approved :: MIT License",
23
- "Typing :: Typed",
24
- "Programming Language :: Python :: 3.8",
25
- "Programming Language :: Python :: 3.9",
26
- "Programming Language :: Python :: 3.10",
27
- "Programming Language :: Python :: 3.11",
28
- "Programming Language :: Python :: 3.12",
29
- ],
30
- )
File without changes
File without changes
File without changes