EmailBuddy (AI Assistant)
EmailBuddy: AI Email Assistant
Section titled “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
Section titled “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?
Section titled “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 |
Prerequisites & Key Concepts
Section titled “Prerequisites & Key Concepts”- Completed Part 2: Add AI (AI integration basics)
- Familiar with OSP Tutorial (nodes, edges, walkers)
| Concept | Where to Learn |
|---|---|
by llm() functions | byLLM Quickstart, Part 2 |
| Agentic patterns | Agentic AI Tutorial |
| Nodes & graph modeling | OSP Tutorial |
| Walker APIs | Part 3 |
The Problem
Section titled “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
Section titled “Architecture”Graph Structure
Section titled “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| P1Data Model
Section titled “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
Section titled “Key Components”1. Building the Graph
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Running EmailBuddy”Start the Server
Section titled “Start the Server”jac start server.jacAPI documentation at http://localhost:8000/docs
Upload Emails
Section titled “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
Section titled “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
Section titled “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
Section titled “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
sem, not manual prompts - Scale-native deploys anywhere - Same code runs locally or in the cloud
Common Pitfalls
Section titled “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
Section titled “Full Source Code”Next Examples
Section titled “Next Examples”- RAG Chatbot - Document Q&A with vector search
- LittleX - Social media platform