exa-py 1.0.9__tar.gz → 1.0.12__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.1
2
2
  Name: exa_py
3
- Version: 1.0.9
3
+ Version: 1.0.12
4
4
  Summary: Python SDK for Exa API.
5
5
  Home-page: https://github.com/exa-labs/exa-py
6
6
  Author: Exa
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
  from dataclasses import dataclass
3
3
  import dataclasses
4
+ from functools import wraps
4
5
  import re
5
6
  import requests
6
7
  from typing import (
8
+ Callable,
9
+ Iterable,
7
10
  List,
8
11
  Optional,
9
12
  Dict,
@@ -15,6 +18,19 @@ from typing import (
15
18
  )
16
19
  from typing_extensions import TypedDict
17
20
 
21
+ import httpx
22
+ from openai import NOT_GIVEN, NotGiven, OpenAI
23
+ from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam
24
+ from openai.types.chat_model import ChatModel
25
+ from exa_py.utils import (
26
+ ExaOpenAICompletion,
27
+ add_message_to_messages,
28
+ format_exa_result,
29
+ maybe_get_query,
30
+ )
31
+
32
+
33
+
18
34
 
19
35
  def snake_to_camel(snake_str: str) -> str:
20
36
  """Convert snake_case string to camelCase.
@@ -319,7 +335,7 @@ class Exa:
319
335
  self,
320
336
  api_key: Optional[str],
321
337
  base_url: str = "https://api.exa.ai",
322
- user_agent: str = "exa-py 1.0.9",
338
+ user_agent: str = "exa-py 1.0.12",
323
339
  ):
324
340
  """Initialize the Exa client with the provided API key and optional base URL and user agent.
325
341
 
@@ -646,3 +662,125 @@ class Exa:
646
662
  [Result(**to_snake_case(result)) for result in data["results"]],
647
663
  data["autopromptString"] if "autopromptString" in data else None,
648
664
  )
665
+ def wrap(self, client: OpenAI):
666
+ """Wrap an OpenAI client with Exa functionality.
667
+
668
+ After wrapping, any call to `client.chat.completions.create` will be intercepted and enhanced with Exa functionality.
669
+
670
+ To disable Exa functionality for a specific call, set `use_exa="none"` in the call to `client.chat.completions.create`.
671
+
672
+ Args:
673
+ client (OpenAI): The OpenAI client to wrap.
674
+
675
+ Returns:
676
+ OpenAI: The wrapped OpenAI client.
677
+ """
678
+
679
+ func = client.chat.completions.create
680
+
681
+ @wraps(func)
682
+ def create_with_rag(
683
+ # Mandatory OpenAI args
684
+ messages: Iterable[ChatCompletionMessageParam],
685
+ model: Union[str, ChatModel],
686
+ # Exa args
687
+ use_exa: Optional[Literal["required", "none", "auto"]] = "auto",
688
+ highlights: Union[HighlightsContentsOptions, Literal[True], None] = None,
689
+ num_results: Optional[int] = 3,
690
+ include_domains: Optional[List[str]] = None,
691
+ exclude_domains: Optional[List[str]] = None,
692
+ start_crawl_date: Optional[str] = None,
693
+ end_crawl_date: Optional[str] = None,
694
+ start_published_date: Optional[str] = None,
695
+ end_published_date: Optional[str] = None,
696
+ use_autoprompt: Optional[bool] = True,
697
+ type: Optional[str] = None,
698
+ category: Optional[str] = None,
699
+ result_max_len: int = 2048,
700
+ # OpenAI args
701
+ **openai_kwargs,
702
+ ):
703
+ exa_kwargs = {
704
+ "num_results": num_results,
705
+ "include_domains": include_domains,
706
+ "exclude_domains": exclude_domains,
707
+ "highlights": highlights,
708
+ "start_crawl_date": start_crawl_date,
709
+ "end_crawl_date": end_crawl_date,
710
+ "start_published_date": start_published_date,
711
+ "end_published_date": end_published_date,
712
+ "use_autoprompt": use_autoprompt,
713
+ "type": type,
714
+ "category": category,
715
+ }
716
+
717
+ create_kwargs = {
718
+ "model": model,
719
+ **openai_kwargs,
720
+ }
721
+
722
+ if use_exa != "none":
723
+ assert "tools" not in create_kwargs, "Tool use is not supported with Exa"
724
+ create_kwargs["tool_choice"] = use_exa
725
+
726
+ return self._create_with_tool(
727
+ create_fn=func,
728
+ messages=list(messages),
729
+ max_len=result_max_len,
730
+ create_kwargs=create_kwargs,
731
+ exa_kwargs=exa_kwargs,
732
+ )
733
+
734
+ print("Wrapping OpenAI client with Exa functionality.", type(create_with_rag))
735
+ client.chat.completions.create = create_with_rag # type: ignore
736
+
737
+ return client
738
+
739
+ def _create_with_tool(
740
+ self,
741
+ create_fn: Callable,
742
+ messages: List[ChatCompletionMessageParam],
743
+ max_len,
744
+ create_kwargs,
745
+ exa_kwargs,
746
+ ) -> ExaOpenAICompletion:
747
+ tools = [
748
+ {
749
+ "type": "function",
750
+ "function": {
751
+ "name": "search",
752
+ "description": "Search the web for relevant information.",
753
+ "parameters": {
754
+ "type": "object",
755
+ "properties": {
756
+ "query": {
757
+ "type": "string",
758
+ "description": "The query to search for.",
759
+ },
760
+ },
761
+ "required": ["query"],
762
+ },
763
+ },
764
+ }
765
+ ]
766
+
767
+ create_kwargs["tools"] = tools
768
+
769
+ completion = create_fn(messages=messages, **create_kwargs)
770
+
771
+ query = maybe_get_query(completion)
772
+
773
+ if not query:
774
+ return ExaOpenAICompletion.from_completion(completion=completion, exa_result=None)
775
+
776
+ exa_result = self.search_and_contents(query, **exa_kwargs)
777
+ exa_str = format_exa_result(exa_result, max_len=max_len)
778
+ new_messages = add_message_to_messages(completion, messages, exa_str)
779
+ # For now, don't allow recursive tool calls
780
+ create_kwargs["tool_choice"] = "none"
781
+ completion = create_fn(messages=new_messages, **create_kwargs)
782
+
783
+ exa_completion = ExaOpenAICompletion.from_completion(
784
+ completion=completion, exa_result=exa_result
785
+ )
786
+ return exa_completion
@@ -0,0 +1,78 @@
1
+ import json
2
+ from typing import Optional
3
+ from openai.types.chat import ChatCompletion
4
+
5
+ from typing import TYPE_CHECKING
6
+ if TYPE_CHECKING:
7
+ from exa_py.api import ResultWithText, SearchResponse
8
+
9
+
10
+
11
+ def maybe_get_query(completion) -> str | None:
12
+ """Extract query from completion if it exists."""
13
+ if completion.choices[0].message.tool_calls:
14
+ for tool_call in completion.choices[0].message.tool_calls:
15
+ if tool_call.function.name == "search":
16
+ query = json.loads(tool_call.function.arguments)["query"]
17
+ return query
18
+ return None
19
+
20
+
21
+ def add_message_to_messages(completion, messages, exa_result) -> list[dict]:
22
+ """Add assistant message and exa result to messages list. Also remove previous exa call and results."""
23
+ assistant_message = completion.choices[0].message
24
+ assert assistant_message.tool_calls, "Must use this with a tool call request"
25
+ # Remove previous exa call and results to prevent blowing up history
26
+ messages = [
27
+ message
28
+ for message in messages
29
+ if not (message.get("role") == "function")
30
+ ]
31
+
32
+ messages.extend([
33
+ assistant_message,
34
+ {
35
+ "role": "tool",
36
+ "name": "search",
37
+ "tool_call_id": assistant_message.tool_calls[0].id,
38
+ "content": exa_result,
39
+ }
40
+ ])
41
+
42
+ return messages
43
+
44
+
45
+ def format_exa_result(exa_result, max_len: int=-1):
46
+ """Format exa result for pasting into chat."""
47
+ str = [
48
+ f"Url: {result.url}\nTitle: {result.title}\n{result.text[:max_len]}\n"
49
+ for result in exa_result.results
50
+ ]
51
+
52
+ return "\n".join(str)
53
+
54
+
55
+ class ExaOpenAICompletion(ChatCompletion):
56
+ """Exa wrapper for OpenAI completion."""
57
+ def __init__(self, exa_result: Optional["SearchResponse[ResultWithText]"], **kwargs):
58
+ super().__init__(**kwargs)
59
+ self.exa_result = exa_result
60
+
61
+
62
+ @classmethod
63
+ def from_completion(
64
+ cls,
65
+ exa_result: Optional["SearchResponse[ResultWithText]"],
66
+ completion: ChatCompletion
67
+ ):
68
+
69
+ return cls(
70
+ exa_result=exa_result,
71
+ id=completion.id,
72
+ choices=completion.choices,
73
+ created=completion.created,
74
+ model=completion.model,
75
+ object=completion.object,
76
+ system_fingerprint=completion.system_fingerprint,
77
+ usage=completion.usage,
78
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: exa-py
3
- Version: 1.0.9
3
+ Version: 1.0.12
4
4
  Summary: Python SDK for Exa API.
5
5
  Home-page: https://github.com/exa-labs/exa-py
6
6
  Author: Exa
@@ -3,6 +3,7 @@ setup.py
3
3
  exa_py/__init__.py
4
4
  exa_py/api.py
5
5
  exa_py/py.typed
6
+ exa_py/utils.py
6
7
  exa_py.egg-info/PKG-INFO
7
8
  exa_py.egg-info/SOURCES.txt
8
9
  exa_py.egg-info/dependency_links.txt
@@ -1,2 +1,3 @@
1
1
  requests
2
2
  typing-extensions
3
+ openai
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="exa_py",
5
- version="1.0.9",
5
+ version="1.0.12",
6
6
  description="Python SDK for Exa API.",
7
7
  long_description_content_type="text/markdown",
8
8
  long_description=open("README.md").read(),
@@ -14,6 +14,7 @@ setup(
14
14
  install_requires=[
15
15
  "requests",
16
16
  "typing-extensions",
17
+ "openai"
17
18
  ],
18
19
  classifiers=[
19
20
  "Development Status :: 5 - Production/Stable",
File without changes
File without changes
File without changes
File without changes