
Exa Rag
- 109 installs
- 5 repo stars
- Updated June 25, 2026
- ejirocodes/agent-skills
Build RAG pipelines with Exa.ai real-time web retrieval, integrating with LangChain, LlamaIndex, Vercel AI SDK, or Claude MCP tool calling.
About
Shows how to wire Exa.ai web search into retrieval-augmented generation across LangChain, LlamaIndex, Vercel AI SDK, and OpenAI-compatible or MCP tool calls. A developer uses it when adding grounded, cited web search to an AI agent or chat app.
- Framework-specific retrievers: ExaSearchRetriever, ExaReader, Vercel AI SDK tools
- Guidance on highlights, summaries, and source attribution for grounded RAG
Exa Rag by the numbers
- 109 all-time installs (skills.sh)
- Ranked #4,092 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ejirocodes/agent-skills --skill exa-ragAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 109 |
|---|---|
| repo stars | ★ 5 |
| Last updated | June 25, 2026 |
| Repository | ejirocodes/agent-skills ↗ |
What it does
Build RAG pipelines with Exa.ai real-time web retrieval, integrating with LangChain, LlamaIndex, Vercel AI SDK, or Claude MCP tool calling.
Files
Exa RAG Integration
Quick Reference
| Topic | When to Use | Reference |
|---|---|---|
| LangChain | Building RAG chains with LangChain | langchain.md |
| LlamaIndex | Using Exa as a LlamaIndex data source | llamaindex.md |
| Vercel AI SDK | Adding web search to Next.js AI apps | vercel-ai.md |
| MCP & Tools | Claude MCP server, OpenAI tools, function calling | mcp-tools.md |
Essential Patterns
LangChain Retriever
from langchain_exa import ExaSearchRetriever
retriever = ExaSearchRetriever(
exa_api_key="your-key",
k=5,
highlights=True
)
docs = retriever.invoke("latest AI research papers")LlamaIndex Reader
from llama_index.readers.web import ExaReader
reader = ExaReader(api_key="your-key")
documents = reader.load_data(
query="machine learning best practices",
num_results=10
)Vercel AI SDK Tool
import { exa } from "@agentic/exa";
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";
const result = await generateText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
prompt: "Search for the latest TypeScript features",
});OpenAI-Compatible Endpoint
from openai import OpenAI
client = OpenAI(
base_url="https://api.exa.ai/v1",
api_key="your-exa-key"
)
response = client.chat.completions.create(
model="exa",
messages=[{"role": "user", "content": "What are the latest AI trends?"}]
)Integration Selection
| Framework | Best For | Key Feature |
|---|---|---|
| LangChain | Complex chains, agents | ExaSearchRetriever, tool integration |
| LlamaIndex | Document indexing, Q&A | ExaReader, query engines |
| Vercel AI SDK | Next.js apps, streaming | Tool definitions, edge-ready |
| OpenAI Compat | Drop-in replacement | Minimal code changes |
| Claude MCP | Claude Desktop, Claude Code | Native tool calling |
Common Mistakes
1. Not using highlights for RAG - Full text wastes context; use highlights=True for relevant snippets 2. Missing source attribution - Always include result.url in citations for grounded responses 3. Ignoring summaries - summary=True provides concise context without full page overhead 4. Over-fetching results - Start with 3-5 results; more isn't always better for RAG quality 5. Not filtering domains - Use include_domains to limit to authoritative sources 6. Skipping date filters - For current events, always add start_published_date to avoid stale info 7. Forgetting async patterns - Use async retrievers in production for better throughput
LangChain Integration Reference
Table of Contents
---
Installation
pip install langchain-exa---
ExaSearchRetriever
The primary way to use Exa with LangChain for RAG.
Basic Usage
from langchain_exa import ExaSearchRetriever
retriever = ExaSearchRetriever(
exa_api_key="your-key", # or set EXA_API_KEY env var
k=5
)
docs = retriever.invoke("latest developments in quantum computing")
for doc in docs:
print(f"URL: {doc.metadata['url']}")
print(f"Title: {doc.metadata['title']}")
print(f"Content: {doc.page_content[:200]}...")With Highlights
retriever = ExaSearchRetriever(
exa_api_key="your-key",
k=5,
highlights=True,
num_sentences=3
)
docs = retriever.invoke("React Server Components best practices")
# docs contain highlight snippets instead of full textWith Filters
retriever = ExaSearchRetriever(
exa_api_key="your-key",
k=10,
include_domains=["github.com", "stackoverflow.com"],
start_published_date="2024-01-01",
type="neural"
)Configuration Options
| Parameter | Type | Description |
|---|---|---|
exa_api_key | str | API key (or use env var) |
k | int | Number of results |
type | str | "auto", "neural", or "keyword" |
include_domains | list | Limit to these domains |
exclude_domains | list | Exclude these domains |
start_published_date | str | Start date filter (YYYY-MM-DD) |
end_published_date | str | End date filter |
highlights | bool | Return highlights instead of full text |
num_sentences | int | Sentences per highlight |
text_length_limit | int | Max characters for text |
---
ExaSearchResults Tool
For LangChain agents that need web search as a tool.
Basic Tool
from langchain_exa import ExaSearchResults
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain_openai import ChatOpenAI
# Create the tool
search_tool = ExaSearchResults(
exa_api_key="your-key",
max_results=5
)
# Use in an agent
llm = ChatOpenAI(model="gpt-4")
agent = create_openai_functions_agent(llm, [search_tool], prompt)
executor = AgentExecutor(agent=agent, tools=[search_tool])
result = executor.invoke({"input": "What are the latest AI news today?"})Tool with Custom Configuration
search_tool = ExaSearchResults(
exa_api_key="your-key",
max_results=10,
text_length_limit=1000,
highlights=True,
include_domains=["techcrunch.com", "wired.com"]
)---
RAG Chain Patterns
Basic RAG Chain
from langchain_exa import ExaSearchRetriever
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
retriever = ExaSearchRetriever(exa_api_key="your-key", k=5, highlights=True)
llm = ChatOpenAI(model="gpt-4")
prompt = ChatPromptTemplate.from_template("""
Answer the question based on the following context. Include source URLs.
Context:
{context}
Question: {question}
Answer:
""")
def format_docs(docs):
return "\n\n".join(
f"Source: {doc.metadata['url']}\n{doc.page_content}"
for doc in docs
)
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
answer = chain.invoke("What are the key features of Python 3.12?")RAG with Citations
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List
class Citation(BaseModel):
url: str
title: str
snippet: str
class AnswerWithCitations(BaseModel):
answer: str
citations: List[Citation]
def create_cited_answer(docs, question):
context = format_docs(docs)
llm_with_structure = llm.with_structured_output(AnswerWithCitations)
prompt = f"""
Based on the context below, answer the question and cite your sources.
Context:
{context}
Question: {question}
"""
return llm_with_structure.invoke(prompt)Conversational RAG
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain
retriever = ExaSearchRetriever(exa_api_key="your-key", k=5)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
chain = ConversationalRetrievalChain.from_llm(
llm=ChatOpenAI(model="gpt-4"),
retriever=retriever,
memory=memory
)
response = chain.invoke({"question": "What is LangChain?"})
response = chain.invoke({"question": "How does it compare to LlamaIndex?"})---
Agent Integration
ReAct Agent with Exa
from langchain_exa import ExaSearchResults
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain import hub
# Get ReAct prompt
prompt = hub.pull("hwchase17/react")
# Create tools
search = ExaSearchResults(exa_api_key="your-key", max_results=5)
tools = [search]
# Create agent
llm = ChatOpenAI(model="gpt-4")
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({
"input": "Research the latest developments in AI agents and summarize the key trends"
})Multi-Tool Agent
from langchain_exa import ExaSearchResults, ExaFindSimilar
search_tool = ExaSearchResults(
exa_api_key="your-key",
name="web_search",
description="Search the web for current information"
)
similar_tool = ExaFindSimilar(
exa_api_key="your-key",
name="find_similar",
description="Find pages similar to a given URL"
)
tools = [search_tool, similar_tool]
# Use with your preferred agent typeLangGraph Integration
from langgraph.graph import StateGraph, END
from langchain_exa import ExaSearchRetriever
retriever = ExaSearchRetriever(exa_api_key="your-key", k=5)
def search_node(state):
query = state["query"]
docs = retriever.invoke(query)
return {"documents": docs}
def generate_node(state):
docs = state["documents"]
# Generate answer using docs
return {"answer": answer}
# Build graph
workflow = StateGraph(dict)
workflow.add_node("search", search_node)
workflow.add_node("generate", generate_node)
workflow.set_entry_point("search")
workflow.add_edge("search", "generate")
workflow.add_edge("generate", END)
app = workflow.compile()LlamaIndex Integration Reference
Table of Contents
---
Installation
pip install llama-index-readers-web---
ExaReader
The primary way to load web content into LlamaIndex.
Basic Usage
from llama_index.readers.web import ExaReader
reader = ExaReader(api_key="your-key") # or set EXA_API_KEY env var
documents = reader.load_data(
query="machine learning best practices",
num_results=10
)
for doc in documents:
print(f"Source: {doc.metadata['url']}")
print(f"Content: {doc.text[:200]}...")With Search Options
documents = reader.load_data(
query="React Server Components tutorial",
num_results=10,
include_domains=["react.dev", "nextjs.org"],
start_published_date="2024-01-01",
text_length_limit=2000
)With Highlights
documents = reader.load_data(
query="Python async patterns",
num_results=10,
highlights=True,
num_sentences=3
)
# Returns documents with highlight snippetsConfiguration Options
| Parameter | Type | Description |
|---|---|---|
query | str | Search query |
num_results | int | Number of results (default: 10) |
include_domains | list | Limit to these domains |
exclude_domains | list | Exclude these domains |
start_published_date | str | Start date (YYYY-MM-DD) |
end_published_date | str | End date |
highlights | bool | Return highlights |
num_sentences | int | Sentences per highlight |
text_length_limit | int | Max characters |
---
Query Engine Patterns
Basic Query Engine
from llama_index.readers.web import ExaReader
from llama_index.core import VectorStoreIndex
from llama_index.llms.openai import OpenAI
# Load documents from Exa
reader = ExaReader(api_key="your-key")
documents = reader.load_data(
query="GraphQL best practices",
num_results=10
)
# Create index and query engine
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(llm=OpenAI(model="gpt-4"))
# Query
response = query_engine.query("What are the key GraphQL patterns?")
print(response)Query Engine with Citations
from llama_index.core import VectorStoreIndex
from llama_index.core.response_synthesizers import ResponseMode
documents = reader.load_data(query="kubernetes deployment", num_results=10)
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(
response_mode=ResponseMode.COMPACT,
llm=OpenAI(model="gpt-4")
)
response = query_engine.query("How do I deploy to Kubernetes?")
# Access source nodes
for node in response.source_nodes:
print(f"Source: {node.metadata['url']}")
print(f"Score: {node.score}")Chat Engine
from llama_index.core.chat_engine import CondenseQuestionChatEngine
documents = reader.load_data(query="LangChain vs LlamaIndex", num_results=10)
index = VectorStoreIndex.from_documents(documents)
chat_engine = CondenseQuestionChatEngine.from_defaults(
query_engine=index.as_query_engine(),
llm=OpenAI(model="gpt-4")
)
response = chat_engine.chat("What are the differences?")
response = chat_engine.chat("Which is better for RAG?")---
Index Integration
Combining with Existing Index
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
# Existing Chroma collection
chroma_client = chromadb.Client()
collection = chroma_client.get_or_create_collection("my_collection")
vector_store = ChromaVectorStore(chroma_collection=collection)
# Load new documents from Exa
reader = ExaReader(api_key="your-key")
new_docs = reader.load_data(query="latest AI news", num_results=5)
# Add to existing index
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(
new_docs,
storage_context=storage_context
)Refresh with Real-Time Data
def refresh_index_with_exa(index, query, num_results=5):
"""Add fresh web content to an existing index."""
reader = ExaReader(api_key="your-key")
fresh_docs = reader.load_data(
query=query,
num_results=num_results,
start_published_date="2024-01-01" # Recent only
)
# Insert into existing index
for doc in fresh_docs:
index.insert(doc)
return index---
Advanced Patterns
Parallel Loading
import asyncio
from llama_index.readers.web import ExaReader
async def load_multiple_queries(queries):
reader = ExaReader(api_key="your-key")
tasks = [
asyncio.to_thread(
reader.load_data,
query=q,
num_results=5
)
for q in queries
]
results = await asyncio.gather(*tasks)
# Flatten documents
return [doc for docs in results for doc in docs]
queries = ["Python async", "Python typing", "Python testing"]
all_docs = asyncio.run(load_multiple_queries(queries))Custom Node Parser
from llama_index.core.node_parser import SentenceSplitter
from llama_index.readers.web import ExaReader
reader = ExaReader(api_key="your-key")
documents = reader.load_data(query="deep learning", num_results=10)
# Custom chunking for web content
parser = SentenceSplitter(
chunk_size=512,
chunk_overlap=50
)
nodes = parser.get_nodes_from_documents(documents)
# Create index from nodes
index = VectorStoreIndex(nodes)Sub-Question Query Engine
from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool
reader = ExaReader(api_key="your-key")
# Create specialized indexes
ml_docs = reader.load_data(query="machine learning frameworks", num_results=10)
web_docs = reader.load_data(query="web development frameworks", num_results=10)
ml_index = VectorStoreIndex.from_documents(ml_docs)
web_index = VectorStoreIndex.from_documents(web_docs)
# Create tools
ml_tool = QueryEngineTool.from_defaults(
query_engine=ml_index.as_query_engine(),
name="ml_search",
description="Search for machine learning information"
)
web_tool = QueryEngineTool.from_defaults(
query_engine=web_index.as_query_engine(),
name="web_search",
description="Search for web development information"
)
# Sub-question engine
query_engine = SubQuestionQueryEngine.from_defaults(
query_engine_tools=[ml_tool, web_tool],
llm=OpenAI(model="gpt-4")
)
response = query_engine.query(
"Compare ML frameworks with web frameworks in terms of learning curve"
)Metadata Filtering
from llama_index.core.vector_stores import MetadataFilters, FilterCondition
documents = reader.load_data(query="tech news", num_results=20)
index = VectorStoreIndex.from_documents(documents)
# Query with metadata filter
filters = MetadataFilters.from_dicts([
{"key": "domain", "value": "techcrunch.com"}
])
query_engine = index.as_query_engine(filters=filters)
response = query_engine.query("What's the latest from TechCrunch?")MCP & Tool Calling Reference
Table of Contents
---
Claude MCP Server
Exa provides an MCP (Model Context Protocol) server for Claude Desktop and Claude Code.
Installation
npm install -g @anthropic/mcp-exa
# or
npx @anthropic/mcp-exaClaude Desktop Configuration
Add to claude_desktop_config.json:
{
"mcpServers": {
"exa": {
"command": "npx",
"args": ["@anthropic/mcp-exa"],
"env": {
"EXA_API_KEY": "your-api-key"
}
}
}
}Available MCP Tools
| Tool | Description |
|---|---|
exa_search | Search the web with neural/keyword modes |
exa_search_and_contents | Search and retrieve page contents |
exa_find_similar | Find pages similar to a URL |
exa_get_contents | Get contents for known URLs |
Claude Code Usage
Once configured, Claude can use Exa directly:
User: Search for the latest React 19 features
Claude: I'll search for that using Exa.
[Uses exa_search_and_contents tool]
Based on the search results, React 19 includes...---
OpenAI Tool Calling
Function Definition
import openai
from exa_py import Exa
exa = Exa()
tools = [
{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web for current information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
},
"num_results": {
"type": "integer",
"description": "Number of results (default: 5)"
}
},
"required": ["query"]
}
}
}
]
def handle_tool_call(tool_call):
if tool_call.function.name == "web_search":
args = json.loads(tool_call.function.arguments)
results = exa.search_and_contents(
args["query"],
num_results=args.get("num_results", 5),
text=True,
highlights=True
)
return json.dumps([{
"title": r.title,
"url": r.url,
"content": r.highlights or [r.text[:500]]
} for r in results.results])Complete Example
import openai
import json
from exa_py import Exa
client = openai.OpenAI()
exa = Exa()
def chat_with_search(user_message):
messages = [{"role": "user", "content": user_message}]
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools
)
# Handle tool calls
while response.choices[0].message.tool_calls:
tool_calls = response.choices[0].message.tool_calls
messages.append(response.choices[0].message)
for tool_call in tool_calls:
result = handle_tool_call(tool_call)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools
)
return response.choices[0].message.content
answer = chat_with_search("What are the latest AI developments?")---
OpenAI Compatibility Mode
Exa provides an OpenAI-compatible endpoint for drop-in replacement.
Basic Usage
from openai import OpenAI
# Point to Exa's OpenAI-compatible endpoint
client = OpenAI(
base_url="https://api.exa.ai/v1",
api_key="your-exa-api-key"
)
response = client.chat.completions.create(
model="exa",
messages=[
{"role": "user", "content": "What are the latest trends in AI?"}
]
)
print(response.choices[0].message.content)With Streaming
stream = client.chat.completions.create(
model="exa",
messages=[
{"role": "user", "content": "Summarize recent AI news"}
],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")TypeScript
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.exa.ai/v1",
apiKey: process.env.EXA_API_KEY,
});
const response = await client.chat.completions.create({
model: "exa",
messages: [{ role: "user", content: "What's happening in tech?" }],
});---
Anthropic Tool Use
Tool Definition
import anthropic
from exa_py import Exa
client = anthropic.Anthropic()
exa = Exa()
tools = [
{
"name": "web_search",
"description": "Search the web for information using Exa",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
},
"num_results": {
"type": "integer",
"description": "Number of results",
"default": 5
}
},
"required": ["query"]
}
}
]Complete Flow
def chat_with_claude_and_exa(user_message):
messages = [{"role": "user", "content": user_message}]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=tools,
messages=messages
)
# Handle tool use
while response.stop_reason == "tool_use":
tool_use = next(
block for block in response.content
if block.type == "tool_use"
)
# Execute Exa search
results = exa.search_and_contents(
tool_use.input["query"],
num_results=tool_use.input.get("num_results", 5),
text=True,
highlights=True
)
tool_result = [{
"title": r.title,
"url": r.url,
"content": r.highlights or [r.text[:500]]
} for r in results.results]
# Continue conversation
messages = [
{"role": "user", "content": user_message},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(tool_result)
}]
}
]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=tools,
messages=messages
)
return response.content[0].text---
CrewAI Integration
Installation
pip install crewai crewai-toolsExa Tool
from crewai import Agent, Task, Crew
from crewai_tools import ExaSearchTool
# Create Exa search tool
search_tool = ExaSearchTool(
api_key="your-exa-key",
n_results=5
)
# Create agent with Exa
researcher = Agent(
role="Research Analyst",
goal="Research and summarize topics thoroughly",
backstory="Expert researcher with web search capabilities",
tools=[search_tool],
verbose=True
)
# Create task
research_task = Task(
description="Research the latest developments in AI agents",
agent=researcher,
expected_output="A comprehensive summary with sources"
)
# Run crew
crew = Crew(
agents=[researcher],
tasks=[research_task]
)
result = crew.kickoff()Multi-Agent with Exa
from crewai import Agent, Task, Crew, Process
search_tool = ExaSearchTool(api_key="your-key")
# Research agent
researcher = Agent(
role="Researcher",
goal="Find relevant information",
tools=[search_tool]
)
# Writer agent
writer = Agent(
role="Writer",
goal="Create well-written content from research"
)
# Tasks
research = Task(
description="Research {topic}",
agent=researcher
)
write = Task(
description="Write article based on research",
agent=writer,
context=[research]
)
crew = Crew(
agents=[researcher, writer],
tasks=[research, write],
process=Process.sequential
)
result = crew.kickoff(inputs={"topic": "AI trends 2024"})---
Google ADK Integration
from google.adk import Agent
from exa_py import Exa
exa = Exa()
def exa_search(query: str, num_results: int = 5) -> str:
"""Search the web using Exa."""
results = exa.search_and_contents(
query,
num_results=num_results,
text=True,
highlights=True
)
return "\n\n".join([
f"Title: {r.title}\nURL: {r.url}\nContent: {r.highlights or r.text[:500]}"
for r in results.results
])
agent = Agent(
tools=[exa_search],
model="gemini-1.5-pro"
)
response = agent.run("What are the latest AI developments?")Vercel AI SDK Integration Reference
Table of Contents
---
Installation
npm install ai @ai-sdk/openai @agentic/exa
# or
pnpm add ai @ai-sdk/openai @agentic/exa---
Tool Definition
Using @agentic/exa
import { exa } from "@agentic/exa";
// Pre-built tool definitions
const searchTool = exa.searchAndContents;
const findSimilarTool = exa.findSimilarAndContents;Custom Tool Definition
import { tool } from "ai";
import Exa from "exa-js";
import { z } from "zod";
const exaClient = new Exa(process.env.EXA_API_KEY);
const searchWeb = tool({
description: "Search the web for information",
parameters: z.object({
query: z.string().describe("The search query"),
numResults: z.number().default(5).describe("Number of results"),
}),
execute: async ({ query, numResults }) => {
const results = await exaClient.searchAndContents(query, {
numResults,
text: true,
highlights: true,
});
return results.results.map((r) => ({
title: r.title,
url: r.url,
content: r.highlights?.join("\n") || r.text?.slice(0, 500),
}));
},
});---
generateText Patterns
Basic Text Generation with Search
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";
const result = await generateText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
maxSteps: 5,
prompt: "What are the latest developments in AI agents?",
});
console.log(result.text);With Custom System Prompt
const result = await generateText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
system: `You are a research assistant. When searching, always:
1. Use specific queries
2. Include sources in your response
3. Synthesize information from multiple results`,
prompt: "Research the current state of WebAssembly adoption",
});Multiple Tools
import { exa } from "@agentic/exa";
const result = await generateText({
model: openai("gpt-4"),
tools: {
search: exa.searchAndContents,
findSimilar: exa.findSimilarAndContents,
},
maxSteps: 10,
prompt:
"Find articles about React Server Components and then find similar articles",
});---
Streaming Patterns
Basic Streaming
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";
const result = await streamText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
maxSteps: 5,
prompt: "Summarize the latest TypeScript features",
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}With Tool Call Handling
import { streamText } from "ai";
const result = await streamText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
maxSteps: 5,
prompt: "Research quantum computing applications",
onStepFinish: ({ stepType, toolCalls, toolResults }) => {
if (stepType === "tool-result") {
console.log("Search completed:", toolResults);
}
},
});---
Next.js Integration
API Route (App Router)
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
maxSteps: 5,
messages,
});
return result.toDataStreamResponse();
}Client Component
// components/Chat.tsx
"use client";
import { useChat } from "ai/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({
api: "/api/chat",
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Ask anything..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
);
}With Tool Result Display
// components/ChatWithTools.tsx
"use client";
import { useChat } from "ai/react";
export function ChatWithTools() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong>
{m.content}
{/* Display tool invocations */}
{m.toolInvocations?.map((tool, i) => (
<div key={i} className="tool-result">
<small>Searched: {tool.args.query}</small>
{tool.state === "result" && (
<ul>
{tool.result.map((r: any, j: number) => (
<li key={j}>
<a href={r.url}>{r.title}</a>
</li>
))}
</ul>
)}
</div>
))}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
);
}Edge Runtime Support
// app/api/search/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";
export const runtime = "edge";
export async function POST(req: Request) {
const { query } = await req.json();
const result = await streamText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
maxSteps: 3,
prompt: query,
});
return result.toDataStreamResponse();
}---
Error Handling
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";
try {
const result = await generateText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
prompt: "Search for...",
});
} catch (error) {
if (error.name === "AI_ToolExecutionError") {
console.error("Search failed:", error.message);
// Handle Exa API errors
} else {
throw error;
}
}---
Best Practices
Limit Max Steps
// Prevent infinite tool loops
const result = await generateText({
model: openai("gpt-4"),
tools: { search: exa.searchAndContents },
maxSteps: 5, // Reasonable limit
prompt: "...",
});Cache Search Results
import { unstable_cache } from "next/cache";
const cachedSearch = unstable_cache(
async (query: string) => {
const exa = new Exa(process.env.EXA_API_KEY);
return exa.searchAndContents(query, { numResults: 5, text: true });
},
["exa-search"],
{ revalidate: 3600 } // 1 hour
);Type-Safe Results
import { z } from "zod";
const SearchResultSchema = z.object({
title: z.string(),
url: z.string(),
content: z.string(),
});
const searchTool = tool({
description: "Search the web",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => {
const results = await exaClient.searchAndContents(query, {
numResults: 5,
text: true,
});
return results.results.map((r) =>
SearchResultSchema.parse({
title: r.title,
url: r.url,
content: r.text?.slice(0, 500) || "",
})
);
},
});