Skip to content

Add responses API wrapper support #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 156 additions & 55 deletions exa_py/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,8 +933,6 @@ def search_and_contents(
use_autoprompt: Optional[bool] = None,
type: Optional[str] = None,
category: Optional[str] = None,
flags: Optional[List[str]] = None,
moderation: Optional[bool] = None,
subpages: Optional[int] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
Expand Down Expand Up @@ -964,7 +962,6 @@ def search_and_contents(
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
flags: Optional[List[str]] = None,
moderation: Optional[bool] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
Expand Down Expand Up @@ -993,7 +990,6 @@ def search_and_contents(
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
flags: Optional[List[str]] = None,
moderation: Optional[bool] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
Expand Down Expand Up @@ -1021,7 +1017,6 @@ def search_and_contents(
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
flags: Optional[List[str]] = None,
moderation: Optional[bool] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
Expand Down Expand Up @@ -1050,7 +1045,6 @@ def search_and_contents(
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
flags: Optional[List[str]] = None,
moderation: Optional[bool] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
Expand Down Expand Up @@ -1078,8 +1072,6 @@ def search_and_contents(
category: Optional[str] = None,
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
flags: Optional[List[str]] = None,
moderation: Optional[bool] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
Expand Down Expand Up @@ -1107,11 +1099,8 @@ def search_and_contents(
type: Optional[str] = None,
category: Optional[str] = None,
flags: Optional[List[str]] = None,
moderation: Optional[bool] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
filter_empty_results: Optional[bool] = None,
extras: Optional[ExtrasOptions] = None,
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...
Expand Down Expand Up @@ -1483,14 +1472,15 @@ def find_similar_and_contents(
end_published_date: Optional[str] = None,
include_text: Optional[List[str]] = None,
exclude_text: Optional[List[str]] = None,
exclude_source_domain: Optional[bool] = None,
use_autoprompt: Optional[bool] = None,
type: Optional[str] = None,
category: Optional[str] = None,
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
flags: Optional[List[str]] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
extras: Optional[ExtrasOptions] = None,
) -> SearchResponse[ResultWithSummary]: ...

Expand All @@ -1510,43 +1500,17 @@ def find_similar_and_contents(
end_published_date: Optional[str] = None,
include_text: Optional[List[str]] = None,
exclude_text: Optional[List[str]] = None,
exclude_source_domain: Optional[bool] = None,
use_autoprompt: Optional[bool] = None,
type: Optional[str] = None,
category: Optional[str] = None,
flags: Optional[List[str]] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
extras: Optional[ExtrasOptions] = None,
) -> SearchResponse[ResultWithTextAndSummary]: ...

@overload
def find_similar_and_contents(
self,
url: str,
*,
highlights: Union[HighlightsContentsOptions, Literal[True]],
summary: Union[SummaryContentsOptions, Literal[True]],
num_results: Optional[int] = None,
include_domains: Optional[List[str]] = None,
exclude_domains: Optional[List[str]] = None,
start_crawl_date: Optional[str] = None,
end_crawl_date: Optional[str] = None,
start_published_date: Optional[str] = None,
end_published_date: Optional[str] = None,
include_text: Optional[List[str]] = None,
exclude_text: Optional[List[str]] = None,
exclude_source_domain: Optional[bool] = None,
category: Optional[str] = None,
flags: Optional[List[str]] = None,
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
extras: Optional[ExtrasOptions] = None,
) -> SearchResponse[ResultWithHighlightsAndSummary]: ...
) -> SearchResponse[ResultWithTextAndSummary]: ...

@overload
def find_similar_and_contents(
Expand All @@ -1565,14 +1529,13 @@ def find_similar_and_contents(
end_published_date: Optional[str] = None,
include_text: Optional[List[str]] = None,
exclude_text: Optional[List[str]] = None,
exclude_source_domain: Optional[bool] = None,
use_autoprompt: Optional[bool] = None,
type: Optional[str] = None,
category: Optional[str] = None,
flags: Optional[List[str]] = None,
livecrawl_timeout: Optional[int] = None,
livecrawl: Optional[LIVECRAWL_OPTIONS] = None,
filter_empty_results: Optional[bool] = None,
subpages: Optional[int] = None,
subpage_target: Optional[Union[str, List[str]]] = None,
extras: Optional[ExtrasOptions] = None,
) -> SearchResponse[ResultWithTextAndHighlightsAndSummary]: ...

Expand Down Expand Up @@ -1623,20 +1586,19 @@ def find_similar_and_contents(self, url: str, **kwargs):
def wrap(self, client: OpenAI):
"""Wrap an OpenAI client with Exa functionality.

After wrapping, any call to `client.chat.completions.create` will be intercepted
and enhanced with Exa RAG functionality. To disable Exa for a specific call,
set `use_exa="none"` in the `create` method.
After wrapping, any call to `client.chat.completions.create` or `client.responses.create`
will be intercepted and enhanced with Exa RAG functionality.

Args:
client (OpenAI): The OpenAI client to wrap.

Returns:
OpenAI: The wrapped OpenAI client.
"""
# Wrap the classic chat completions API
chat_func = client.chat.completions.create

func = client.chat.completions.create

@wraps(func)
@wraps(chat_func)
def create_with_rag(
# Mandatory OpenAI args
messages: Iterable[ChatCompletionMessageParam],
Expand Down Expand Up @@ -1684,15 +1646,154 @@ def create_with_rag(
}

return self._create_with_tool(
create_fn=func,
create_fn=chat_func,
messages=list(messages),
max_len=result_max_len,
create_kwargs=create_kwargs,
exa_kwargs=exa_kwargs,
)

print("Wrapping OpenAI client with Exa functionality.")
client.chat.completions.create = create_with_rag # type: ignore
# Wrap the responses API
if hasattr(client, 'responses') and hasattr(client.responses, 'create'):
responses_func = client.responses.create

@wraps(responses_func)
def create_with_responses_rag(
model: Union[str, ChatModel],
input: Union[List[dict], List[ChatCompletionMessageParam]],
tools: Optional[List[dict]] = None,
**openai_kwargs
):
# Initialize tools if not provided
tools = tools or []

# Check if web_search_exa tool is included
exa_config = {}
exa_tool_index = None

input = [{
"role": "system",
"content": "You are a helpful assistant. When users ask questions, use the exa_search function to find relevant information and provide detailed answers with citations."
}] + input
for i, tool in enumerate(tools):
if tool.get("type") == "web_search_exa":
# Extract configuration if provided
exa_config = tool.get("config", {})
exa_tool_index = i
break


# Only proceed with Exa integration if web_search_exa was requested
if exa_tool_index is not None:
# Replace the web_search_exa tool with a function tool
tools[exa_tool_index] = {
"type": "function",
"name": "exa_search",
"description": "Search the web using Exa",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
}
},
"required": ["query"]
}
}
else:
# If no web_search_exa tool requested, just pass through to original function
return responses_func(
model=model,
input=input,
tools=tools,
**openai_kwargs
)

# Make initial API call
response = responses_func(
model=model,
input=input,
tools=tools,
**openai_kwargs
)

# Check for function calls
function_call = None
if hasattr(response, 'output'):
for item in response.output:
if hasattr(item, 'type') and item.type == 'function_call' and item.name == 'exa_search':
function_call = item
break

if function_call:
args = json.loads(function_call.arguments)
query = args.get("query")

if query:
num_results = exa_config.get("num_results", 3)
max_chars = exa_config.get("text", {}).get("max_characters", 2000)


try:
search_results = self.search_and_contents(
query=query,
num_results=num_results,
text={"max_characters": max_chars}
)


# Format search results - Fixed string concatenation
results_text_parts = []
for i, result in enumerate(search_results.results):
result_text = f"\nTitle: {result.title}\nURL: {result.url}\n"
if hasattr(result, 'text') and result.text:
result_text += f"Content: {result.text[:500]}...\n"
results_text_parts.append(result_text)

results_text = "".join(results_text_parts)

# Add function call and results to messages
new_input = list(input)
new_input.append({
"type": "function_call",
"name": "exa_search",
"arguments": function_call.arguments,
"call_id": function_call.call_id
})
new_input.append({
"type": "function_call_output",
"call_id": function_call.call_id,
"output": results_text
})

# Send final request with search results
final_response = responses_func(
model=model,
input=new_input,
tools=tools,
**openai_kwargs
)

# Attach search results to response
final_response.exa_result = search_results
return final_response

except Exception as e:
print(f"Error during search: {str(e)}")
print(f"Error type: {type(e)}")
import traceback
print(f"Traceback: {traceback.format_exc()}")
raise

return response

# Apply the wrapper
client.responses.create = create_with_responses_rag

# Apply the chat completions wrapper
client.chat.completions.create = create_with_rag
print("Wrapping OpenAI client with Exa functionality for both Chat Completions and Responses APIs.")

return client

Expand Down
2 changes: 1 addition & 1 deletion examples/rag/Exa_RAG.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@
"metadata": {},
"outputs": [],
"source": [
"paragraph = \"\"\"Georgism, also known as Geoism, is an economic philosophy and ideology named after the American political economist Henry George (1839–1897). This doctrine advocates for the societal collective, rather than individual property owners, to capture the economic value derived from land and other natural resources. To this end, Georgism proposes a single tax on the unimproved value of land, known as a \"land value tax,\" asserting that this would deter speculative land holding and promote efficient use of valuable resources. Adherents argue that because the supply of land is fundamentally inelastic, taxing it will not deter its availability or use, unlike other forms of taxation. Georgism differs from Marxism and capitalism, underscoring the distinction between common and private property while largely contending that individuals should own the fruits of their labor.\"\"\"\n",
"paragraph = \"\"\"Georgism, also known as Georgism, is an economic philosophy and ideology named after the American political economist Henry George (1839–1897). This doctrine advocates for the societal collective, rather than individual property owners, to capture the economic value derived from land and other natural resources. To this end, Georgism proposes a single tax on the unimproved value of land, known as a \"land value tax,\" asserting that this would deter speculative land holding and promote efficient use of valuable resources. Adherents argue that because the supply of land is fundamentally inelastic, taxing it will not deter its availability or use, unlike other forms of taxation. Georgism differs from Marxism and capitalism, underscoring the distinction between common and private property while largely contending that individuals should own the fruits of their labor.\"\"\"\n",
"query = f\"The best academic source about {paragraph} is (paper: \"\n",
"georgism_search_response = exa.search_and_contents(paragraph, highlights=highlights_options, num_results=5, use_autoprompt=False)"
]
Expand Down
2 changes: 1 addition & 1 deletion examples/rag/Exa_RAG.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Exa can be used for more than simple question answering. One superpower of embed


```python
paragraph = """Georgism, also known as Geoism, is an economic philosophy and ideology named after the American political economist Henry George (1839–1897). This doctrine advocates for the societal collective, rather than individual property owners, to capture the economic value derived from land and other natural resources. To this end, Georgism proposes a single tax on the unimproved value of land, known as a "land value tax," asserting that this would deter speculative land holding and promote efficient use of valuable resources. Adherents argue that because the supply of land is fundamentally inelastic, taxing it will not deter its availability or use, unlike other forms of taxation. Georgism differs from Marxism and capitalism, underscoring the distinction between common and private property while largely contending that individuals should own the fruits of their labor."""
paragraph = """Georgism, also known as Georgism, is an economic philosophy and ideology named after the American political economist Henry George (1839–1897). This doctrine advocates for the societal collective, rather than individual property owners, to capture the economic value derived from land and other natural resources. To this end, Georgism proposes a single tax on the unimproved value of land, known as a "land value tax," asserting that this would deter speculative land holding and promote efficient use of valuable resources. Adherents argue that because the supply of land is fundamentally inelastic, taxing it will not deter its availability or use, unlike other forms of taxation. Georgism differs from Marxism and capitalism, underscoring the distinction between common and private property while largely contending that individuals should own the fruits of their labor."""
query = f"The best academic source about {paragraph} is (paper: "
georgism_search_response = exa.search_and_contents(paragraph, highlights=highlights_options, num_results=5, use_autoprompt=False)
```
Expand Down
Loading
Loading