Skip to content

EmailBuddy (AI Assistant)

Build an intelligent email assistant that transforms your inbox into a searchable knowledge graph.

Time: 60 minutes Level: Intermediate


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

EmailBuddy demonstrates three key Jac principles:

ConceptHow It’s Used
Object-Spatial ProgrammingEmails and people as connected nodes
AI Agents (byLLM)LLM-powered graph traversal and decision-making
Scale-NativeWalkers become API endpoints automatically

ConceptWhere to Learn
by llm() functionsbyLLM Quickstart, Part 2
Agentic patternsAgentic AI Tutorial
Nodes & graph modelingOSP Tutorial
Walker APIsPart 3

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.


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
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;
}

When emails are uploaded, EmailBuddy:

  1. Extracts sender and recipient addresses
  2. Creates Person nodes (if they don’t exist)
  3. Creates EmailNode nodes
  4. Connects everything to root
  5. 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)};
}
}

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
}

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();

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;
}
}
}

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.


jac start server.jac

API documentation at http://localhost:8000/docs

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."
}
]
}'
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."
}

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);
}
});

  1. Graphs capture relationships - Emails aren’t just data, they’re connections between people
  2. Walkers explore intelligently - AI-powered traversal finds relevant information
  3. byLLM simplifies agents - Define behavior with types and sem, not manual prompts
  4. Scale-native deploys anywhere - Same code runs locally or in the cloud

MistakeSymptomFix
Nodes not connected to rootWalker can’t find themroot ++> newNode
Duplicate emailsRepeated nodes in graphCheck UUID before creating
Walker runs foreverInfinite traversalUse disengage when done
LLM context overflowPoor answersUse summarization agent