I write to three kinds of people every week, and I sound like a different person to each one.
My manager gets updates on tasks and deliverables. I always respect them by saying their name first:
Priya, these tasks are done.
My clients get messages that are very to the point: a requirement workshop, a feature I'm explaining, a pain point they brought to the project. User adoption is the main key for them. I make sure the empathy is there and that we deliver what we promised.
My college friends get no restriction at all:
hey bro what's up
hey broski
this is amazing, killer outfit
Same me, three personas. And every time I want an AI to help with any of them, I have to leave the screen I'm on.
Purrsona maps how I write to each person, platform and topic, and shows which edits taught it. It lives on a key: plug it in and the cat writes like me; pull it out and the laptop forgets me. Built for Graph Hacks: Context for AI Agents (WeMakeDevs × FalkorDB), Track 02.
Live demo · GitHub · Chrome extension · Demo video

Option+M in a reply box: the cat checks my voice graph and writes like me.
The problem: I have to go somewhere else to get a draft
I'm on LinkedIn, so I navigate out of the window, open a chatbot and paste the link of the post. On Reddit I take a screenshot, because otherwise the chatbot says it doesn't have access to this post.
Then I say "make it sound like me", and it doesn't, unless I've set up its memory and preferences. So I explain how I sound, every time. A lot of tries later, it understands. Then the session is gone, and the context with it. It's very frustrating, for each and every post.
With Purrsona I don't set up anything for how I talk to this person, and this person, and this person. The graph and the cat learn it from what I actually send.

Same post, same shortcut. Top: key in, Friends rules. Bottom: key out, a generic reply.
Why a graph
I learned graphs two weeks before this hackathon, with my favorite game. Chatbots gave me generic Clash Royale advice and didn't know the cards I had. So I built ClashBuddy, a graph of cards, synergies, elixir and legendary status, and asked it how to improve my own game.
A writing voice is the same kind of problem. Give a chatbot my old messages and, at best, it finds ones that look similar. It doesn't see the full view: who each person is to me, which circle they're in, which platform I'm on, and which rule wins when two clash. A graph keeps those as connections, and FalkorDB walks them in milliseconds.

Every rule hangs off a scope and points back at the edit that taught it.
1. Plug in: I carry my cat with me
Say I'm at the airport, flying to another country. I'm not carrying my MacBook; I already have one there. Why should I sign up for a chatbot again and set everything up again?
You carry what it has learned with you. You're carrying your cat with you. I treat it as a pet: I take it everywhere I go, and it remembers who I am and how I talk with people.
That laptop needs the extension and the bridge once, but no account and no re-teaching: I plug the key in, press its button, and my voice graph is rebuilt from what the key carries.
2. Draft: the graph decides, Claude only writes
Purrsona felt real to me when I talked with Mustafa on LinkedIn, then on Gmail, then on Reddit. The cat doesn't think Mustafa is the same everywhere. It drafts from who Mustafa is, what my relation to him is, and how I talk with him on this particular platform. That was the icebreaker for me. And all those chatbot tabs stay closed. I'm just there.
Under the hood, the graph gathers rules from five scopes: the person, their circle, the topic, the platform, and me. The narrowest rule of each kind wins. Kabir isn't in the graph at all, so every lookup is an OPTIONAL MATCH; a plain MATCH would find no Kabir and return nothing. Every read goes through FalkorDB's read-only ro_query. Claude only gets the post and the handful of rules that won. It never sees the graph and never writes a query.
// simplified from RULES_FOR_CONTEXT in api/purrsona/queries.py
UNWIND scopes AS s // Priya, Work, the topic, LinkedIn, me
MATCH (s)-[:HAS_RULE]->(r:StyleRule)
WHERE r.superseded_at IS NULL
WITH r, CASE labels(s)[0] WHEN 'Person' THEN 1 WHEN 'Circle' THEN 2
WHEN 'Topic' THEN 3 WHEN 'Platform' THEN 4 ELSE 5 END AS rank
ORDER BY rank
WITH r.kind AS kind, collect(r.text) AS candidates
RETURN kind, candidates[0] AS winner // one winner per kind
With a fresh key, the cat asks a lot of questions. That's right. I stay on the same post and answer, in my own words, who this person is to me.
3. Learn: one edit becomes a rule
With a chatbot, when a session gets compacted or the conversation gets too large, it forgets who is who. And it's a very tedious task to tell it that I talk with these 250 people, and here are the rules for each. Nobody has the time. An AI should learn that by itself, on the same screen.
A very casual example: at first I talk with my girlfriend in a very romantic way. Now we're married, and we talk about our kids and the electricity bill. Why would I tell an AI that I'm married now? It should learn that from how I talk to my wife.
So when I send, the Learner compares what the cat drafted with what I actually sent, and saves the difference as a rule, linked to the edit that taught it. On Friday I changed a Kabir draft to open with "Bhaila", pulled the key out, plugged it back in, and the next Kabir draft started with "Bhaila".
// SAVE_LEARNED_RULE: the rule, its owner, and the edit that taught it
MATCH (p:Person {id: $person_id}), (e:Edit {id: $edit_id})
CREATE (p)-[:HAS_RULE]->(r:StyleRule {id: $rule_id, kind: $kind, text: $text, created_at: $at})
-[:LEARNED_FROM]->(e)
// LINK_OVERRIDES: it outranks the broader rule, which stays as history
MATCH (r:StyleRule {id: $rule_id})
UNWIND $old_ids AS oid
MATCH (old:StyleRule {id: oid})
CREATE (r)-[:OVERRIDES]->(old)
4. Why: every draft explains itself
I want to know why a draft sounds like me. Why does a draft to Mustafa say "bro"? If, three months from now, I start calling him "broski", I don't tell a chatbot. I just send it that way, the cat learns it, and I can click "why?" to see how the reply was drafted.
I wrote the query behind it myself, step by step in FalkorDB Browser:
// WHY_THIS_DRAFT
MATCH (d:Draft {id: $draft})-[:USED]->(r:StyleRule)<-[:HAS_RULE]-(s)
OPTIONAL MATCH (r)-[:LEARNED_FROM]->(ev)
RETURN r.text AS rule, labels(s)[0] AS scope, coalesce(s.name, s.id) AS owner, count(ev) AS evidence
ORDER BY CASE scope WHEN 'Person' THEN 1 WHEN 'Circle' THEN 2 WHEN 'Topic' THEN 3
WHEN 'Platform' THEN 4 WHEN 'User' THEN 5 ELSE 6 END, evidence DESC
Behind every reply there are three agents. The Drafter writes. The Critic's main job is to make sure the Drafter follows the rules the Learner learned; if a draft broke one, it sends it back once. The Learner learns when I send. They never call each other: every step lands in the graph as a Step node, and every draft shows what the graph ran, each query with its milliseconds and rows. It feels like three people are backing me up.
5. Unplug: the laptop forgets
This is the true portability. And if I want the AI to forget me, I just pull the key out. That's it. The bridge runs GRAPH.DELETE on my graph, and the next draft is generic.
Mustafa built the key, and I built the rest. In his clip, the key shows "Memory connected" with my graph and 35 rules, he pulls the cable, and the laptop says "Forgotten". Plug it back in, press the button, and everything comes back.

The real key on Mustafa's desk: plugged in, then the cable pulled.
What broke (the useful part)
A lot of things broke, and every round of testing on the real sites found something new.
Every site has its own text box. On X, Backspace stopped working after the cat filled the box. On Slack, a script's paste made the box empty itself. So every site gets a paste except Slack, which gets typed, and each site has a small adapter, so a new site is one more adapter.
Words inside words. "bhaila" matched my AI topic, because "ai" is inside it. My rule "Never says 'Bro'" flagged "broke". Both now match whole words, and I wrote the second fix myself.
A Critic that could never be satisfied. A two-sentence reply to Rohan counted as five, because "Hi Rohan,", "Regards," and "Maulik" looked like sentences, and the Clients rules require them. Now it counts only the message body.
The key and the laptop talking. Opening the USB port can reset the ESP32, so the bridge waits two seconds for the boot messages before it speaks. Every unlock is signed, after a button press. On the first real unplug, the graph was deleted but the laptop didn't say "Forgotten", until Mustafa fixed how a lost USB session is reported.
Numbers and honest limits
| 1,000 users, one graph each, 32 lookups at a time | p50 | p95 |
|---|---|---|
| Plug in: build a user's graph from their snapshot | 10.6 ms | 13.9 ms |
| Draft lookup: who the person is, the topics, which rules win | 20.4 ms | 41.3 ms |
Unplug: GRAPH.DELETE
|
0.6 ms | 2.1 ms |
That's 1,424 draft lookups a second, and none of them saw another user's rules.
Not done yet: the key's flash and USB messages are plaintext (on purpose, so the board stays reusable), the Critic doesn't judge tone, and it doesn't learn from my own new posts yet.
Try it
Try the live demo: your own key, your own graph, and a practice page with the cat built in, nothing to install. For LinkedIn, X or Gmail, load the extension from the latest release. The README has a 10-minute local setup.
No API key? Paste these two queries into FalkorDB Browser and watch the narrowest rule win:
CREATE (me:User {name: 'Me'}), (work:Circle {name: 'Work'}),
(priya:Person {name: 'Priya'}), (neha:Person {name: 'Neha'}),
(me)-[:KNOWS]->(priya), (me)-[:KNOWS]->(neha),
(priya)-[:IN_CIRCLE]->(work), (neha)-[:IN_CIRCLE]->(work),
(work)-[:HAS_RULE]->(:StyleRule {kind: 'emoji', text: 'No emojis'}),
(priya)-[:HAS_RULE]->(:StyleRule {kind: 'emoji', text: 'One emoji is fine for wins'})
MATCH (p:Person)-[:IN_CIRCLE]->(c:Circle)
UNWIND [[p, 1], [c, 2]] AS scope
WITH p, scope[0] AS owner, scope[1] AS rank
MATCH (owner)-[:HAS_RULE]->(r:StyleRule)
WITH p, r, rank ORDER BY rank
RETURN p.name AS person, r.kind AS kind, collect(r.text)[0] AS wins
When I open the dashboard, I see my graph. These are my nodes, these are my neurons, this is where my blood is flowing.

My voice graph, live from FalkorDB.
I'm very sure this is a pain most developers, and most people, are facing. So try Purrsona and tell me: did you go to a chatbot less? Type less? Can you relate, or is there a tool you already use that solves it? Tell me what's missing. I'd like to build it.
Plug in anywhere. Unplug, and the AI forgets.






Top comments (0)