What is Exa?
Exa is the search engine built for AI. It finds information from across the web and delivers both links and the actual content from pages, making it easy to use with AI models.
Exa uses neural search technology to understand the meaning of queries, not just keywords. The API works with both semantic search and traditional keyword methods.
Get Started
First, you’ll need API keys from both OpenAI and Exa:
Python Example
Here’s a complete example using Python:
import json
from openai import OpenAI
from exa_py import Exa
OPENAI_API_KEY = ""
EXA_API_KEY = ""
tools = [{
"type": "function",
"name": "exa_websearch",
"description": "Search the web using Exa. Provide relevant links in your answer.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query for Exa."
}
},
"required": ["query"],
"additionalProperties": False
},
"strict": True
}]
system_message = {"role": "system", "content": "You are a helpful assistant. Use exa_websearch to find info when relevant. Always list sources."}
def run_exa_search(user_query):
"""Run an Exa web search with a dynamic user query."""
openai_client = OpenAI(api_key=OPENAI_API_KEY)
exa = Exa(api_key=EXA_API_KEY)
messages = [
system_message,
{"role": "user", "content": user_query}
]
print("Sending initial request to OpenAI...")
response = openai_client.responses.create(
model="gpt-4o",
input=messages,
tools=tools
)
print("Initial OpenAI response:", response.output)
function_call = None
for item in response.output:
if item.type == "function_call" and item.name == "exa_websearch":
function_call = item
break
if function_call:
call_id = function_call.call_id
args = json.loads(function_call.arguments)
query = args.get("query", "")
print(f"\nOpenAI requested a web search for: {query}")
search_results = exa.search_and_contents(
query=query,
text = {
"max_characters": 4000
}
type="auto"
)
citations = [{"url": result.url, "title": result.title} for result in search_results.results]
search_results_str = str(search_results)
messages.append({
"type": "function_call",
"name": function_call.name,
"arguments": function_call.arguments,
"call_id": call_id
})
messages.append({
"type": "function_call_output",
"call_id": call_id,
"output": search_results_str
})
print("\nSending search results back to OpenAI for a final answer...")
response = openai_client.responses.create(
model="gpt-4o",
input=messages,
tools=tools
)
if hasattr(response, 'output_text') and response.output_text:
formatted_response = format_response_with_citations(response.output_text, citations)
if hasattr(response, 'model_dump'):
response_dict = response.model_dump()
else:
response_dict = response.dict() if hasattr(response, 'dict') else response.__dict__
if response.output and len(response.output) > 0:
response_dict['output'] = [{
"type": "message",
"id": response.output[0].id if hasattr(response.output[0], 'id') else "msg_custom",
"status": "completed",
"role": "assistant",
"content": [{
"type": "output_text",
"text": formatted_response["text"],
"annotations": formatted_response["annotations"]
}]
}]
response_dict['output_text'] = formatted_response["text"]
try:
response = type(response)(**response_dict)
except:
print("\nFormatted response with citations would be:", formatted_response)
print("\nFinal Answer:\n", response.output_text)
print("\nAnnotations:", json.dumps(response.output[0].content[0].annotations if hasattr(response, 'output') and response.output and hasattr(response.output[0], 'content') else [], indent=2))
print("\nFull Response with Citations:", response)
return response
def format_response_with_citations(text, citations):
"""Format the response to include citations as annotations."""
annotations = []
formatted_text = text
for i, citation in enumerate(citations):
start_index = len(formatted_text)
citation_text = f"\n\n[{i+1}] {citation['url']}"
end_index = start_index + len(citation_text)
annotation = {
"type": "url_citation",
"start_index": start_index,
"end_index": end_index,
"url": citation["url"],
"title": citation["title"]
}
annotations.append(annotation)
formatted_text += citation_text
return {
"text": formatted_text,
"annotations": annotations
}
if __name__ == "__main__":
user_query = input("Enter your question: ")
run_exa_search(user_query)
JavaScript Example
Here’s the same example using JavaScript:
const OpenAI = require('openai');
const exaModule = require('exa-js');
const Exa = exaModule.default;
const openai = new OpenAI({ apiKey: '' });
const exa = new Exa('');
const tools = [{
type: 'function',
name: 'exa_websearch',
description: 'Search the web using Exa. Provide relevant links in your answer.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for Exa.'
}
},
required: ['query'],
additionalProperties: false
},
strict: true,
}];
const systemMessage = { role: 'system', content: 'You are a helpful assistant. Use exa_websearch to find info when relevant. Always list sources.' };
async function run_exa_search(userQuery) {
const messages = [
systemMessage,
{ role: 'user', content: userQuery }
];
console.log('Sending initial request to OpenAI...');
let response = await openai.responses.create({
model: 'gpt-4o',
input: messages,
tools
});
console.log('Initial OpenAI Response:', JSON.stringify(response, null, 2));
const functionCall = response.output.find(item =>
item.type === 'function_call' && item.name === 'exa_websearch');
if (functionCall) {
const query = JSON.parse(functionCall.arguments).query;
const searchResults = await exa.searchAndContents(query, {
type: 'auto',
text: {
maxCharacters: 4000
}
});
const citations = searchResults.results.map(result => ({
url: result.url,
title: result.title
}));
messages.push(functionCall);
messages.push({
type: 'function_call_output',
call_id: functionCall.call_id,
output: JSON.stringify(searchResults)
});
console.log('Sending follow-up request with search results to OpenAI...');
response = await openai.responses.create({
model: 'gpt-4o',
input: messages,
tools
});
if (response.output_text) {
const formattedResponse = formatResponseWithCitations(response.output_text, citations);
const customResponse = {
...response,
output: [
{
type: "message",
id: response.output[0].id,
status: "completed",
role: "assistant",
content: [
{
type: "output_text",
text: formattedResponse.text,
annotations: formattedResponse.annotations
}
]
}
],
output_text: formattedResponse.text
};
response = customResponse;
}
}
console.log('Final Answer:\n', response.output_text);
console.log('Annotations:', JSON.stringify(response.output[0]?.content[0]?.annotations || [], null, 2));
console.log('Response with Citations:', JSON.stringify(response, null, 2));
return response;
}
function formatResponseWithCitations(text, citations) {
const annotations = [];
let formattedText = text;
citations.forEach((citation, index) => {
const annotation = {
type: "url_citation",
start_index: formattedText.length,
end_index: formattedText.length + citation.url.length + 3,
url: citation.url,
title: citation.title
};
annotations.push(annotation);
formattedText += `\n\n[${index + 1}] ${citation.url}`;
});
return {
text: formattedText,
annotations
};
}
async function runExaSearchExample() {
const userQuery = process.argv[2] || "What's the latest news about AI?";
const result = await run_exa_search(userQuery);
return result;
}
if (require.main === module) {
runExaSearchExample().catch(console.error);
}
module.exports = { run_exa_search };
Both examples show how to:
- Set up the OpenAI Response API with Exa as a tool
- Make a request to OpenAI
- Handle the search function call
- Send the search results back to OpenAI
- Get the final response
Remember to replace the empty API key strings with your actual API keys when trying these examples.
Let’s break down how the Exa web search tool works with OpenAI’s Response API:
-
Tool Definition: First, we define our Exa search as a tool that OpenAI can use:
-
Initial Request: When you send a message to OpenAI, the API looks at your message and decides if it needs to search the web. If it does, instead of giving a direct answer, it will return a “function call” in its output.
-
Function Call: If OpenAI decides to search, it returns something like:
-
Search Execution: Your code then:
- Takes this search query
- Calls Exa’s API to perform the actual web search
- Gets real web results back
-
Final Response: You send these web results back to OpenAI, and it gives you a final answer using the fresh information from the web.
This back-and-forth process happens automatically in the code above, letting OpenAI use Exa’s web search when it needs to find current information.