← Back to list

Building a Family Tree AI Assistant: From GEDCOM to WhatsApp Bot with a Graph Database

How I turned a GEDCOM export from MyHeritage into a multi-agent system that answers “who is my cousin?” in Polish, Czech, and English

Piotr Brudny · 2026-02-28 20:29 · 4 claps · 7.0 min read
#neo4j #gedcom #whatsapp #ai-assistant #python
Open on Medium ↗
Wiki topics: AGT · AI Agents AI · AI · General 👨‍👩‍👧 · Family & Parenting

Building a Family Tree AI Assistant: From GEDCOM to WhatsApp Bot with a Graph Database

How I turned a GEDCOM export from MyHeritage into a multi-agent system that answers “who is my cousin?” in Polish, Czech, and English

There is a WhatsApp group called something like “Kowalski Family” in almost every extended family I know. Mine has 34 members spread across Poland, Czech Republic, and a handful of other countries. We share birthday wishes, holiday photos, and — inevitably — the occasional message that starts with: “Wait, how are we related again?”

I am a software developer. And I had just discovered that MyHeritage lets you export your entire family tree as a GEDCOM file — a standardized genealogy format that has existed since the 1980s. Those two facts collided one weekend into a project I did not plan to build but could not stop building: an AI assistant that lets any family member ask natural-language questions about our family tree, directly from the WhatsApp group we already use.

This is the story of how I built it.

The Problem With Family Trees

MyHeritage (and services like it) are excellent at storing genealogical data. They have beautiful interfaces, relationship maps, and smart matching features. But they have one significant limitation for a distributed family: not everyone wants to create an account and learn a new tool just to answer a simple question.

My 74-year-old aunt does not want to log in to anything. She just wants to know whether the woman in the old photograph is her grandmother’s sister or her grandmother’s cousin. And she wants to ask in Polish.

My Czech relatives have the same problem, in Czech.

What I needed was a zero-friction interface — something the family was already using. The WhatsApp group was the obvious answer.

The Architecture: GEDCOM → Graph → AI → WhatsApp

Before writing a single line of code, I mapped out what the system needed to do:

  1. Import the GEDCOM file from MyHeritage into a proper graph database
  2. Accept natural-language questions from WhatsApp (or a web interface)
  3. Detect the language of the question (Polish, Czech, or English)
  4. Convert the question into a database query
  5. Format the result as a natural, friendly answer in the same language

The choice of database was easy: Neo4j. Family trees are graphs. People are nodes, relationships are edges. Querying “who are the cousins of person X” is a natural graph traversal, not a SQL join nightmare.

For the AI layer, I used OpenAI’s GPT-4o mini — capable enough for the task, cheap enough to run for a family hobby project.

For WhatsApp integration, I chose Twilio’s WhatsApp Business API, which lets you set up a sandbox in minutes without going through Meta’s lengthy business verification process.

Here is the high-level architecture:

User (WhatsApp or Web)
        ↓
  Channel Adapter        ← normalise input, filter triggers
        ↓
  Orchestrator Agent
    ├── Language Detection Agent   → pl / cs / en
    ├── Cypher Generation Agent    → Neo4j Cypher query
    ├── Neo4j                      → execute, return records
    └── Response Formatting Agent  → natural language answer
        ↓
  Answer (in the user's language)

Each step is a small, focused AI agent with a strict system prompt. This “multi-agent” pattern — rather than one giant prompt trying to do everything — made the system much more reliable and easier to debug.

Step 1: Importing the Family Tree

MyHeritage exports a .ged file. GEDCOM is a text format that looks like this:

0 @I1@ INDI
1 NAME Jan /Kowalski/
1 SEX M
1 BIRT
2 DATE 15 MAR 1942
2 PLAC Kraków
0 @F1@ FAM
1 HUSB @I1@
1 WIFE @I2@
1 CHIL @I3@

I parsed this with the python-gedcom library and loaded it into Neo4j using MERGE statements — meaning re-importing the same file is safe and idempotent. Each individual becomes a Person node; each family record becomes PARENT_OF and MARRIED_TO relationships.

The graph schema ended up beautifully simple:

(:Person)-[:PARENT_OF]->(:Person)
(:Person)-[:MARRIED_TO]->(:Person)
(:Person)-[:LIVES_IN]->(:Place)
(:Person)-[:BORN_IN]->(:Place)

No explicit “cousin” or “uncle” relationships. Those are derived at query time by traversing the graph. This keeps the data model clean and mirrors how a real family tree works.

Step 2: The Language Detection Agent

The first agent in the pipeline is the simplest. Its entire system prompt is:

Detect the language of the user’s message. Reply with ONLY one of these three codes: pl, cs, en. No punctuation, no explanation, no extra text.

I call GPT-4o mini with temperature=0 and max_tokens=5. It returns pl, cs, or en. If it returns anything else, I default to en. This agent costs a fraction of a cent per call and is remarkably reliable.

Step 3: The Cypher Generation Agent

This is the heart of the system. The agent receives the user’s question and detected language, and must return a valid, read-only Neo4j Cypher query.

The system prompt is more elaborate here. It defines the entire graph schema, explains every relationship, and — critically — enforces strict rules:

  • ALWAYS start the query with: MATCH (me:Person {id: $userId})
  • Output raw Cypher only — no markdown, no code fences, no explanation
  • Use ONLY these clauses: MATCH, WHERE, WITH, RETURN, ORDER BY, LIMIT
  • NEVER use: CREATE, DELETE, MERGE, CALL, DROP

The $userId parameter is key. Every query is anchored to the person asking. When someone asks "who are my cousins?", the generated Cypher starts from their node in the graph. This makes the system feel personal — each family member gets answers relative to their own position in the tree.

For complex relationships like cousins, the agent learns to navigate the graph:

MATCH (me:Person {id: $userId})
MATCH (me)<-[:PARENT_OF]-(parent)<-[:PARENT_OF]-(grandparent)
MATCH (grandparent)-[:PARENT_OF]->(uncle_aunt)
  WHERE uncle_aunt <> parent
MATCH (uncle_aunt)-[:PARENT_OF]->(cousin)
RETURN cousin.firstName AS firstName, cousin.lastName AS lastName
ORDER BY cousin.lastName, cousin.firstName

Step 4: Security — Because This Matters

Before executing any generated Cypher, I run it through a validator that checks for forbidden keywords:

FORBIDDEN_KEYWORDS = re.compile(
    r"\b(CREATE|DELETE|MERGE|DROP|CALL|SET|REMOVE|FOREACH)\b",
    re.IGNORECASE,
)
def validate_cypher(query: str) -> None:
    match = FORBIDDEN_KEYWORDS.search(query)
    if match:
        raise ValueError(f"Forbidden keyword: {match.group()}")

All queries also use parameterised values — $userId rather than string interpolation — so there is no risk of injection attacks. The Neo4j role used by the AI has read-only permissions at the database level. Even if someone crafted a prompt that tricked the model into generating a destructive query, it would fail at three separate layers.

I also cap every result at 100 records and enforce a 5-second query timeout.

Step 5: The Response Formatting Agent

Raw database results look like this:

[
  {"firstName": "Marta", "lastName": "Kowalska"},
  {"firstName": "Piotr", "lastName": "Kowalski"}
]

The formatting agent converts this into natural prose, in the right language, with appropriate context. Its system prompt includes:

Be warm and conversational — this is a family app. If the list has more than 10 items, summarize. If no data is found, clearly say so in the correct language.

The same query asked in Polish returns:

Twoje kuzynki i kuzyni to: Marta Kowalska i Piotr Kowalski.

Asked in Czech:

Tvoji bratranci a sestřenice jsou: Marta Kowalská a Petr Kowalský.

Step 6: The WhatsApp Integration

Twilio’s WhatsApp sandbox is genuinely easy to set up. You configure a webhook URL, and Twilio sends a POST request to it whenever someone messages your sandbox number.

One important design decision: the bot does not respond to every message. In a family WhatsApp group, the bot is a participant among 34 people. If it replied to every message, it would be insufferable. So I filter:

def should_respond(msg: ChannelMessage) -> bool:
    if msg.channel == Channel.WEB:
        return True  # Always respond on web
    # WhatsApp: only if message starts with ! or / or mentions the bot
    for prefix in ["!", "/"]:
        if msg.text.startswith(prefix):
            return True
    if "familybot" in msg.text.lower():
        return True
    return False

Family members who want to ask something prefix their message with !. Everyone else's conversation is left alone.

The bot also needs to know who is asking. Each WhatsApp number is mapped to a Person node in the admin panel:

POST /admin/map-whatsapp
{"personId": "I1", "whatsappId": "whatsapp:+48123456789"}

Once mapped, the bot looks up the sender’s number and uses their Person node as the starting point for every query. “My grandmother” means something different depending on who is asking.

The Web Interface

Not everyone in the family uses the WhatsApp group. For them — and for development and testing — I built a minimal React/Vite chat interface. You enter your Person ID, and you get a clean chat window that sends questions to the same backend pipeline.

It is not a work of art. It is a single page with a message list, a text input, and a send button. That is all it needs to be.

What I Learned

Graph databases are the right tool for this problem. I briefly considered using a relational database with a complex schema of join tables. After five minutes of thinking about the cousin query in SQL, I abandoned that idea.

Multi-agent pipelines are more debuggable than monolithic prompts. When something goes wrong, I know immediately whether it was the language detection, the Cypher generation, or the response formatting. Each agent has a single job and can be tested independently.

The hardest part was identity. Knowing who is asking and making every query relative to that person transforms the experience from “family tree search” to “personal assistant who knows your family”. This required a clear data model and consistent use of the $userId parameter from the start.

Prompt engineering for structured output requires strict rules. The Cypher generation agent produces invalid output surprisingly often without firm constraints. The combination of a strict system prompt, post-generation validation, and a fallback error message gives the system enough robustness for family use.

What’s Next

The system works. My aunt asked it who her grandmother’s sister was. She got the answer in Polish without creating any accounts or learning any tools. That felt like a win.

A few things I want to add:

  • LIVES_IN relationships — where family members currently live (useful for planning reunions)
  • A family graph viewer — a visual rendering of the tree in the web interface
  • Smarter memory — letting the bot remember context across a multi-turn conversation (“and what about her husband?”)
  • Photo support — GEDCOM can include references to photos; making those retrievable via chat would be wonderful

The full source code is open on GitHub: github.com/pbrudny/my-family-bot

If you have a family tree sitting in MyHeritage and a WhatsApp group full of relatives asking each other how they are related, this might be the weekend project for you too.

Built with: Python · FastAPI · OpenAI · Neo4j · Twilio · React · Docker


메타데이터
post_id
b1fcf0b3cc9e
slug
building-a-family-tree-ai-assistant-from-gedcom-to-whatsapp-bot-with-a-graph-database-b1fcf0b3cc9e
url
https://medium.com/@pbrudny/building-a-family-tree-ai-assistant-from-gedcom-to-whatsapp-bot-with-a-graph-database-b1fcf0b3cc9e
canonical_url
https://medium.com/@pbrudny/building-a-family-tree-ai-assistant-from-gedcom-to-whatsapp-bot-with-a-graph-database-b1fcf0b3cc9e
author_url
https://medium.com/@pbrudny
status
ok
fetched_at
2026-07-13 06:23:13