EmailBuddy: AI Email Assistant#
Build an intelligent email assistant that transforms your inbox into a searchable knowledge graph.
Time: 60 minutes Level: Intermediate
What You'll Build#
EmailBuddy is an agentic AI email assistant that:
- Stores emails as an interconnected graph
- Answers questions about your email history
- Summarizes conversations and threads
- Uses semantic search to find relevant messages
Why This Example?#
EmailBuddy demonstrates three key Jac principles:
| Concept | How It's Used |
|---|---|
| Object-Spatial Programming | Emails and people as connected nodes |
| AI Agents (byLLM) | LLM-powered graph traversal and decision-making |
| Scale-Native | Walkers become API endpoints automatically |
The Problem#
Your inbox is a flat list of messages. Finding "what was the final price we agreed on?" requires:
- Keyword searching
- Manual digging
- Scrolling through threads
EmailBuddy transforms this into a graph you can query naturally.
Architecture#
Graph Structure#
graph LR
R((Root)) --> P1[Person: Josh]
R --> P2[Person: John]
R --> P3[Person: Sarah]
P1 -->|sent| E1(Email 1)
E1 -->|to| P2
E1 -->|to| P3
P2 -->|sent| E2(Email 2)
E2 -->|to| P1
Data Model#
node Person {
has name: str;
has email: str;
}
node EmailNode {
has sender: str;
has recipients: str;
has date: str;
has subject: str;
has body: str;
has email_uuid: str;
}
Key Components#
1. Building the Graph#
When emails are uploaded, EmailBuddy:
- Extracts sender and recipient addresses
- Creates Person nodes (if they don't exist)
- Creates EmailNode nodes
- Connects everything to root
- Creates directed edges: person → email → recipients
walker upload_emails {
has emails: list[dict];
can process with `root entry {
for email in self.emails {
# Create or find sender
sender = find_or_create_person(email["from"]);
# Create email node
email_node = EmailNode(
sender=email["from"],
recipients=email["to"],
date=email["date"],
subject=email["subject"],
body=email["body"],
email_uuid=generate_uuid(email)
);
# Connect to root
root ++> email_node;
# Connect sender to email
sender ++> email_node;
# Connect email to recipients
for recipient in parse_recipients(email["to"]) {
recipient_node = find_or_create_person(recipient);
email_node ++> recipient_node;
}
}
report {"uploaded": len(self.emails)};
}
}
2. Finding Nodes with Walkers#
Helper walkers traverse the graph to find specific nodes:
walker FindSenderNode {
has target: str;
has person: Person = None;
can start with `root entry {
visit [-->];
return self.person;
}
can search with Person entry {
if here.email == self.target {
self.person = here;
disengage;
}
}
}
Usage:
with entry {
finder = FindSenderNode(target="alice@example.com");
root spawn finder;
sender = finder.person; # Found Person node or None
}
3. AI-Powered Navigation#
The key innovation: an LLM decides how to traverse the graph.
import from byllm.lib { Model }
glob llm = Model(model_name="gpt-4o-mini");
obj Response {
has option: str; # @selected@, @query@, or @end@
has selection: str; # Chosen node, query, or answer
has explanation: str; # Why this decision
}
sem Response = "Structured response for agentic traversal.";
sem Response.option = "Control token: @selected@, @query@, or @end@.";
"""Decide which option is best: explore an email, search for more, or answer."""
def choose_next_email_node(
person: str,
sent: list[str],
received: list[str],
conversation_history: list[dict]
) -> Response by llm();
4. The Query Walker#
The main walker uses the AI agent to answer questions:
walker ask_email {
has query: str;
has conversation_history: list[dict] = [];
can start with `root entry {
# Append user query to history
self.conversation_history.append({
"role": "user",
"content": self.query
});
# Start exploration
visit [-->](`?Person);
}
can explore with Person entry {
# Gather context from current person
sent_emails = [here -->](`?EmailNode);
received_emails = [<-- here](`?EmailNode);
# Ask AI what to do next
response = choose_next_email_node(
here.name,
format_emails(sent_emails),
format_emails(received_emails),
self.conversation_history
);
if response.option == "@selected@" {
# Visit selected email
visit [-->](`?EmailNode).filter(
lambda e: any -> bool { e.email_uuid == response.selection; }
);
} elif response.option == "@query@" {
# Semantic search for more emails
results = semantic_search(response.selection);
visit results;
} elif response.option == "@end@" {
# Return final answer
report {"answer": response.selection};
disengage;
}
}
}
Summarization Agent#
Keep the LLM context efficient by summarizing discoveries:
"""Summarize relevant information from emails for the conversation."""
def summarize(
presented_options: list[str],
convo_history: list[dict]
) -> str by llm();
This prevents the context window from overflowing as the walker explores more nodes.
Running EmailBuddy#
Start the Server#
API documentation at http://localhost:8000/docs
Upload Emails#
curl -X POST http://localhost:8000/walker/upload_emails \
-H "Content-Type: application/json" \
-d '{
"emails": [
{
"date": "2025-01-15T10:00:00Z",
"from": "alice@example.com",
"to": "bob@example.com",
"subject": "Project update",
"body": "The final price is $5,000."
}
]
}'
Query the Assistant#
curl -X POST http://localhost:8000/walker/ask_email \
-H "Content-Type: application/json" \
-d '{"query": "What was the final price we agreed on?"}'
Response:
{
"answer": "Based on your email with Alice on January 15th, the final price agreed upon was $5,000."
}
Web Interface#
EmailBuddy includes a web chat interface:
// Query the walker from JavaScript
$.ajax({
url: 'http://localhost:8000/walker/ask_email',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ query: message }),
success: function(response) {
displayAnswer(response.answer);
}
});
Key Takeaways#
- Graphs capture relationships - Emails aren't just data, they're connections between people
- Walkers explore intelligently - AI-powered traversal finds relevant information
- byLLM simplifies agents - Define behavior with types and docstrings, not prompts
- Scale-native deploys anywhere - Same code runs locally or in the cloud
Common Pitfalls#
| Mistake | Symptom | Fix |
|---|---|---|
| Nodes not connected to root | Walker can't find them | root ++> newNode |
| Duplicate emails | Repeated nodes in graph | Check UUID before creating |
| Walker runs forever | Infinite traversal | Use disengage when done |
| LLM context overflow | Poor answers | Use summarization agent |
Full Source Code#
Next Examples#
- RAG Chatbot - Document Q&A with vector search
- LittleX - Social media platform