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.
- {exa_py-1.0.9 → exa_py-1.0.12}/PKG-INFO +1 -1
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py/api.py +139 -1
- exa_py-1.0.12/exa_py/utils.py +78 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py.egg-info/PKG-INFO +1 -1
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py.egg-info/SOURCES.txt +1 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py.egg-info/requires.txt +1 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/setup.py +2 -1
- {exa_py-1.0.9 → exa_py-1.0.12}/README.md +0 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py/__init__.py +0 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py/py.typed +0 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py.egg-info/dependency_links.txt +0 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/exa_py.egg-info/top_level.txt +0 -0
- {exa_py-1.0.9 → exa_py-1.0.12}/setup.cfg +0 -0
|
@@ -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.
|
|
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
|
+
)
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="exa_py",
|
|
5
|
-
version="1.0.
|
|
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
|
|
File without changes
|
|
File without changes
|