
building a llama_index functioncallingagent for email (with real infrastructure)
How to connect a LlamaIndex FunctionCallingAgent to real email infrastructure so your agent can send and receive messages, not just call dummy functions.
Most LlamaIndex agent tutorials end at the same place: a send_email tool that prints to the console. The agent "sends" a message, the function returns "Email sent successfully", and everyone moves on. It's fine for learning the API. It's useless for production.
If you want your LlamaIndex FunctionCallingAgent to actually send and receive email, you need infrastructure behind those tool definitions. That means real inboxes, real deliverability, and real security considerations that notebook demos never touch.
This guide walks through building a function calling email agent with LlamaIndex, then connecting it to actual email infrastructure so the messages go somewhere.
What is a FunctionCallingAgent in LlamaIndex?#
A FunctionCallingAgent is a LlamaIndex agent that uses an LLM's native function calling API (sometimes called "tool use") to decide which tools to invoke. Instead of parsing text output to figure out what the agent wants to do (the ReAct approach), the LLM returns structured tool calls directly. This makes tool invocation more reliable and less prone to parsing errors.
The key difference between FunctionAgent and ReActAgent: FunctionAgent relies on the LLM's built-in tool calling support, while ReActAgent uses prompting to reason through tool selection. FunctionAgent is generally more reliable for well-defined tools like email operations, where the inputs are structured and predictable. ReActAgent is better when you need the agent to reason through ambiguous situations.
LLMs that support function calling in LlamaIndex include OpenAI (GPT-4o, GPT-4), Anthropic (Claude 3 and later), Mistral (Large, Medium), and several others through community integrations.
How to build a LlamaIndex FunctionCallingAgent for email#
Here's the process, step by step:
- Install
llama-index-core,llama-index-llms-openai(or your preferred LLM provider), and@lobsterkit/lobstermailfor email infrastructure - Define your email tool functions:
send_email,read_inbox, and optionallyreply_to_email - Wrap each function with
FunctionTool.from_defaults()to register it with the agent - Initialize a
FunctionAgentwith your LLM and tool list - Create an inbox for the agent to use
- Run the agent with a natural language prompt like "Check my inbox and reply to any unread messages"
- Handle errors and edge cases (bounced emails, rate limits, empty inboxes)
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI
# Define the tools (we'll make these real in the next section)
send_tool = FunctionTool.from_defaults(fn=send_email)
read_tool = FunctionTool.from_defaults(fn=read_inbox)
agent = FunctionAgent(
tools=[send_tool, read_tool],
llm=OpenAI(model="gpt-4o"),
system_prompt="You are an email assistant. Use your tools to manage email."
)
response = await agent.run("Check my inbox for new messages")
That's the skeleton. Now let's make the tools do something real.
## Defining email tools that actually work
The gap in every LlamaIndex email tutorial is the tool implementation. Here's what a typical tutorial gives you:
```python
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to the specified address."""
print(f"Sending to {to}: {subject}")
return "Email sent successfully"
This teaches you nothing about what happens when your agent tries to send 50 follow-up emails and half of them bounce. Or when the receiving server returns a 550 because your sending domain has no SPF record. Or when your agent needs to read a verification code from an incoming email to complete a signup flow.
Real email tools need real infrastructure behind them. Here's what that looks like using LobsterMail's SDK, where the agent provisions its own inbox without any human configuration:
```typescript
import { LobsterMail } from '@lobsterkit/lobstermail';
const lm = await LobsterMail.create();
const inbox = await lm.createSmartInbox({ name: 'my-llama-agent' });
// This is the function your LlamaIndex tool wraps
async function sendEmail(to: string, subject: string, body: string) {
return await inbox.send({ to, subject, body });
}
async function readInbox() {
const emails = await inbox.receive();
return emails.map(e => ({
from: e.from,
subject: e.subject,
preview: e.preview,
risk_score: e.security?.injectionScore
}));
}
The `createSmartInbox` call gives the agent an address like `my-llama-agent@lobstermail.ai`. No DNS records, no SMTP credentials, no OAuth dance. The agent handles provisioning itself.
## Connecting Python tools to a TypeScript SDK
If you're building your agent in Python with LlamaIndex but want to use a Node-based email SDK, you have a few options. The most straightforward: use the LobsterMail REST API directly from Python.
```python
import httpx
LOBSTERMAIL_TOKEN = "lm_sk_live_..."
BASE = "https://api.lobstermail.ai/v1"
async def send_email(to: str, subject: str, body: str) -> str:
"""Send an email from the agent's inbox."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{BASE}/inboxes/{INBOX_ID}/send",
headers={"Authorization": f"Bearer {LOBSTERMAIL_TOKEN}"},
json={"to": to, "subject": subject, "body": body}
)
if resp.status_code == 200:
return f"Email sent to {to}"
return f"Failed: {resp.json().get('error', 'unknown')}"
async def read_inbox() -> str:
"""Read recent emails from the agent's inbox."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{BASE}/inboxes/{INBOX_ID}/emails",
headers={"Authorization": f"Bearer {LOBSTERMAIL_TOKEN}"}
)
emails = resp.json().get("emails", [])
return str([{
"from": e["from"],
"subject": e["subject"],
"preview": e["preview"]
} for e in emails])
Then wrap these as LlamaIndex tools:
send_tool = FunctionTool.from_defaults(
fn=send_email,
name="send_email",
description="Send an email to a recipient with a subject and body"
)
read_tool = FunctionTool.from_defaults(
fn=read_inbox,
name="read_inbox",
description="Read recent emails from the agent's inbox"
)
Memory and context management#
When your agent handles email conversations, context matters. The ChatMemoryBuffer in LlamaIndex stores previous interactions so the agent can reference earlier emails in a thread.
from llama_index.core.memory import ChatMemoryBuffer
memory = ChatMemoryBuffer.from_defaults(token_limit=4096)
agent = FunctionAgent(
tools=[send_tool, read_tool],
llm=OpenAI(model="gpt-4o"),
memory=memory,
system_prompt="You are an email assistant. Remember previous conversations."
)
For email agents, memory is where things get interesting. The agent can read an email, draft a reply, and remember the context if the recipient responds later. Without memory, every interaction starts from scratch.
One thing to watch: memory buffers fill up fast when you're injecting full email bodies. Consider summarizing emails before storing them, or only keeping the most recent 5-10 exchanges.
Security when your agent has email access#
Here's the part nobody talks about in LlamaIndex tutorials: what happens when your agent can read and write real emails?
An agent with unrestricted email access can be prompted (through a malicious email body) to forward sensitive information, reply with credentials, or send spam. This is prompt injection through email, and it's a real attack vector.
A few safeguards worth building in:
Scope the agent's permissions. If it only needs to read, don't give it the send tool. If it only needs to send to specific addresses, validate the to field in your tool function before calling the API.
Use injection scoring. LobsterMail returns a security risk score with every received email. You can filter out high-risk messages before the agent ever sees them:
async def read_inbox_safe() -> str:
"""Read inbox, filtering out emails with high injection risk."""
emails = await raw_read_inbox()
safe = [e for e in emails if e.get("risk_score", 1.0) < 0.7]
return str(safe)
Add human approval for outbound email. For production agents, consider a review step where the agent drafts the email but a human approves it before sending. You can implement this as a tool that queues the draft rather than sending immediately.
Moving from notebook to production#
Notebook demos run once and stop. A production email agent needs to run continuously, checking for new emails and responding to them.
The simplest deployment pattern: a cron job or scheduled task that runs every few minutes, checks for new emails, and processes them. No persistent server required.
For real-time responsiveness, use webhooks. When a new email arrives at the agent's inbox, a webhook fires and triggers the agent to process it. This avoids polling and handles emails within seconds.
Either way, add observability. Log every tool call, every email sent, every error. When your agent sends an email to the wrong person at 3 AM, you want a trail to follow.
Where to go from here#
If you're building a LlamaIndex FunctionCallingAgent for email, start with the read-only case. Get your agent reading and summarizing emails before you let it send anything. Add the send tool once you're confident in the agent's judgment, and add a human approval gate if the stakes are high.
For the email infrastructure side, LobsterMail's getting started guide covers inbox provisioning and the full API surface. The free tier gives you 1,000 emails per month, which is plenty for development and light production use.
Frequently asked questions
What is a LlamaIndex FunctionCallingAgent and how does it differ from a standard LLM chain?
A FunctionCallingAgent uses the LLM's native tool-calling API to invoke functions directly, rather than generating text that gets parsed. Unlike a basic LLM chain, it can take actions (send email, query a database) and reason about the results across multiple steps.
Which LLMs support function calling in LlamaIndex?
OpenAI (GPT-4o, GPT-4, GPT-3.5-turbo), Anthropic (Claude 3+), and Mistral (Large, Medium) all support function calling through their respective LlamaIndex integrations. Check the llama-index-llms-* packages for the latest provider support.
What is the difference between FunctionAgent and ReActAgent in LlamaIndex?
FunctionAgent uses the LLM's built-in tool calling API for structured invocation. ReActAgent uses prompting to reason through tool selection in a thought-action-observation loop. FunctionAgent is more reliable for well-defined tools; ReActAgent handles ambiguous reasoning better.
How do I define a send_email tool for a LlamaIndex agent?
Write a Python function with type hints and a docstring, then wrap it with FunctionTool.from_defaults(fn=your_function). The docstring becomes the tool description the LLM uses to decide when to call it.
How do I connect a LlamaIndex agent to real email infrastructure instead of a dummy function?
Replace the dummy function body with API calls to a real email service. You can use LobsterMail's REST API, or any SMTP/transactional email provider. The tool function signature stays the same; only the implementation changes.
How does ChatMemoryBuffer work inside a FunctionCallingAgent?
ChatMemoryBuffer stores conversation history up to a token limit, letting the agent reference previous interactions. For email agents, this means it can remember earlier messages in a thread and draft contextual replies.
How do I handle errors when an email tool raises an exception?
Return error information as a string from your tool function rather than raising exceptions. The agent can then reason about the error and decide whether to retry, skip, or ask for help. For example: return f"Failed to send: {error_message}".
Can a LlamaIndex FunctionCallingAgent handle email attachments?
Yes, but you need to design your tool function to accept attachment data (file paths or base64-encoded content) and pass it to your email API. The LLM will call the tool with attachment parameters if you include them in the function signature.
How do I prevent a LlamaIndex agent from sending emails without human approval?
Create a draft_email tool instead of send_email. The tool saves the draft to a queue, and a separate process lets a human review and approve before actually sending. This keeps the agent functional while adding a safety gate.
What are the rate limit considerations when an AI agent sends emails at scale?
Most email providers enforce per-minute and per-day send limits. Exceeding them causes temporary blocks or permanent reputation damage. Build rate limiting into your tool function, and monitor bounce rates closely. LobsterMail's free tier allows 1,000 emails per month; the Builder tier raises that limit.
How do I deploy a LlamaIndex email agent as a persistent service?
The simplest approach is a cron job that runs every few minutes. For real-time processing, use webhooks to trigger the agent when new emails arrive. Either way, add logging and error handling so you can debug issues after the fact.
Is LobsterMail free to use for development?
Yes. The free tier includes 1,000 emails per month, no credit card required. Your agent can sign up automatically through the SDK without any human intervention.


