diff --git a/exa_py/api.py b/exa_py/api.py index 5576915..66d9991 100644 --- a/exa_py/api.py +++ b/exa_py/api.py @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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]: ... @@ -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]: ... @@ -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( @@ -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]: ... @@ -1623,9 +1586,8 @@ 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. @@ -1633,10 +1595,10 @@ def wrap(self, client: OpenAI): 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], @@ -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 diff --git a/examples/rag/Exa_RAG.ipynb b/examples/rag/Exa_RAG.ipynb index e8fa063..831360a 100644 --- a/examples/rag/Exa_RAG.ipynb +++ b/examples/rag/Exa_RAG.ipynb @@ -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)" ] diff --git a/examples/rag/Exa_RAG.md b/examples/rag/Exa_RAG.md index 83d7d0d..c635dfb 100644 --- a/examples/rag/Exa_RAG.md +++ b/examples/rag/Exa_RAG.md @@ -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) ``` diff --git a/examples/rag/Exa_RAG.py b/examples/rag/Exa_RAG.py index 0fa2cb8..7f1303e 100644 --- a/examples/rag/Exa_RAG.py +++ b/examples/rag/Exa_RAG.py @@ -116,7 +116,7 @@ # In[8]: -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) diff --git a/examples/rag/Exa_RAG_responses.py b/examples/rag/Exa_RAG_responses.py new file mode 100644 index 0000000..4bd85ae --- /dev/null +++ b/examples/rag/Exa_RAG_responses.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# coding: utf-8 + +# # RAG: Answer your questions with Exa and OpenAI's Responses API + +# This example demonstrates how to use Exa with OpenAI's new responses API +# for retrieval-augmented generation. + +# Install the required packages: +# pip install exa-py openai + +from exa_py import Exa +from openai import OpenAI +import os + +# Set up Exa client +my_exa_api_key = os.environ.get("EXA_API_KEY") +if not my_exa_api_key: + raise ValueError("EXA_API_KEY environment variable not set") +exa = Exa(my_exa_api_key) + +# Set up OpenAI client +openai_api_key = os.environ.get("OPENAI_API_KEY") +if not openai_api_key: + raise ValueError("OPENAI_API_KEY environment variable not set") +openai_client = OpenAI(api_key=openai_api_key) + +# Wrap the OpenAI client with Exa functionality +openai_client = exa.wrap(openai_client) + +# Define some questions to answer +questions = [ + "How did bats evolve their wings?", + "How did Rome defend Italy from Hannibal?", +] + +# Function to get answers using the responses API with Exa integration +def get_answer_with_exa(question): + """Get an answer using Exa search through OpenAI's Responses API.""" + response = openai_client.responses.create( + model="gpt-4o", + 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." + }, + { + "role": "user", + "content": question + } + ], + tools=[{ + "type": "web_search_exa", + "config": { + "num_results": 3, + "text": { + "max_characters": 2000 + } + } + }] + ) + + # Extract answer text from the response + answer = "" + if hasattr(response, 'output'): + for item in response.output: + if hasattr(item, 'content'): + answer += item.content[0].text + + # Format the response with question and answer + formatted_response = { + "question": question, + "answer": answer, + } + return formatted_response + + +# Get answers for all questions +responses = [] +for question in questions: + response = get_answer_with_exa(question) + responses.append(response) + +# Display the results +print("\n=== RESPONSES WITH EXA SEARCH ===\n") +for response in responses: + if response: + print(f"Question: {response['question']}") + print(f"Answer: {response['answer']}") + print("\n---\n") + else: + print(f"No response found for question: {question}") + +# Example for a more complex query with paragraph input +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.""" + +# Get academic sources using responses API +response = openai_client.responses.create( + model="gpt-4o", + input=[ + {"role": "user", "content": f"Find the best academic sources about the following economic theory: {paragraph}"} + ], + tools=[ + { + "type": "web_search_exa", + "config": { + "num_results": 5 + } + } + ] +) + +print("\n=== ACADEMIC SOURCES FOR GEORGISM ===\n") +answer = "" +for item in response.output: + if item.type == "text": + answer += item.text + +print(answer) + +if hasattr(response, "exa_result"): + print("\n=== RAW EXA SEARCH RESULTS ===\n") + for result in response.exa_result.results: + print(f"Title: {result.title}") + print(f"URL: {result.url}") + if hasattr(result, 'highlights') and result.highlights: + print("Highlight:", result.highlights[0]) + print()