Audit report — 2026-08-19

How accurate is the conversation topic labeler? Final report

What this is: Every conversation Clara has (calls, texts, emails) gets tagged with a “topic” — leasing, maintenance, tour, renewal, billing, turnover, resident question, vendor, or “no topic” — so the team can filter, search, and see what’s happening on the conversations list and the portfolio dashboard. This report checks how often that tag is right.

How we checked it: We had another AI read each conversation’s messages and judge whether the stored topic tag matches what the conversation is actually about. That’s an automated judge working from message excerpts, not a human reading every conversation — treat the numbers below as a solid estimate, not verified ground truth. We also ran a stress test: re-labeling 25 real conversations a second time to see if the labeler agrees with itself, and throwing deliberately tricky inputs at it (foreign language, spam wrappers, buried requests) to find its breaking points.


1. Headline numbers — and the one fact that changes everything

Across all 2,698 conversations the judge looked at:

Count %
Judged 2,698 100%
Labeled correctly (OK) 1,899 70.4%
Labeled wrong (WRONG) 733 27.2%
Judge couldn’t tell (UNSURE) 66 2.4%

Read on its own, a 70% correct rate sounds bad. It’s the wrong number to act on, because it blends together two very different populations that got audited side by side:

Splitting the same audit by property tells a completely different story:

Property What it is Judged Correct % correct
Camellia Apartments Live customer 708 668 94.4%
Propflow.ai Test Property Internal test data 1,111 586 52.7%
The Willows Designated test tenant 529 326 61.6%
Yale 25 Station (+ sandbox copy) Early pilot / test 33 17 51.5%

On the live customer’s real conversations, the labeler is right 94.4% of the time, wrong 2.8% of the time, and the judge was unsure the other 2.8%. Almost all of the “wrong” volume in the blended number comes from test and pilot properties — see why in the next section.

(This property breakdown comes from the underlying per-conversation records we still have on disk, which cover every single one of the 733 wrong cases and about 90% of the full judged set — enough to trust the split completely for the “wrong” bucket, which is what matters here.)


2. What’s actually going wrong — by pattern

Pattern 1 — Never labeled at all (the big one, but not a customer problem)

650 of the 733 “wrong” cases (89%) are conversations where no topic was ever stamped — the field is just empty, not “wrong topic,” just blank. Of those 650:

Examples: conv-renewal-tenant-gauntlet-appfolio-45-1779657416937 (scripted test SMS, never triggered labeling), conv_harness_prove_turnover_e2e_2026 (test harness conversation), 002d5104-dbbd-40f1-8d79-88b40ca251ec (Test Property, tour logistics question, never labeled).

Bottom line: this isn’t the labeler picking wrong topics — it’s old test data and pre-launch history that was never run through the labeler at all, and it’s sitting almost entirely outside the live customer’s data.

Pattern 2 — Spam, dead air, and wrong numbers get a real topic instead of “no topic” (8 real-customer cases)

This is the largest genuine mistake pattern on live customer data. The labeler is supposed to say “no topic” for silence, robocalls, wrong numbers, and people who just want a human — but in these 8 cases it instead borrowed a topic from the leftover menu text or picked something arbitrary:

Pattern 3 — Leasing prospects get mistaken for a “unit turnover” (4 real-customer cases)

Four calls where someone is clearly a new prospect asking about apartments got tagged “turnover” (the process of getting a unit ready between tenants) instead of “leasing”: conv_voice_44207cb9... (a straightforward new-lease call), conv_voice_5e686751... (caller literally says “Leasing,” asks about pet policy), conv_voice_85a7a836... (prospect arriving to sign a new lease). This same confusion shows up even more often in the test-property data, so it’s a recurring blind spot, not a one-off.

Pattern 4 — Third-party vendor reps mistaken for leasing prospects (2 real-customer cases)

Two calls from data-aggregator/listing-verification companies (a “Smart Locating” rep and a “Smart Apartment Data” rep, both calling to check occupancy/pricing for their own database) got tagged “leasing” as if they were real prospects: conv_voice_304727d3..., conv_voice_52e620da.... Small in count, but it’s the kind of thing that would quietly inflate a “how many leasing leads did we get” number if anyone trusted it.

Pattern 5 — Topic boundary confusion: billing vs. maintenance/resident question (2 real-customer cases, including a Spanish-language miss)

conv_voice_3d64eb1a... — a call about getting electricity put in the tenant’s name got tagged “billing” (it’s not a payment issue). conv_voice_4785b753... — a Spanish-language call about a maintenance work order (“orden de trabajo”) got tagged “billing” instead of “maintenance.” Worth flagging: our stress test (see Section 4) found Spanish-language input was not a weak spot in synthetic tests — this real example shows the risk isn’t zero in practice, just uncommon.

Pattern 6 — A second topic in the same conversation gets dropped (recurring on test data, worth watching)

A common shape, mostly on test-property data but a plausible risk for real conversations too: a call covers two things — say, a lease renewal and a broken AC — and only one topic gets saved. Examples: conv_voice_1a1759d3... and conv_voice_1aebc884... (renewal discussion + billing question, only “billing” saved), conv_voice_240448dc... (maintenance issue + renewal discussion, only “maintenance” saved). This didn’t show up in the 20 real-customer misses, but it’s common enough elsewhere that it’s likely happening quietly on real calls too.

Pattern 7 — Automated-notification wrappers and buried requests (proven in the stress test, not yet caught live)

Not found in the real-conversation sample above, but proven directly by the stress test in Section 4: when a real request (like a vendor confirming a delivery) is wrapped in boilerplate — the header text a system like AppFolio or Zillow adds to a forwarded message — the actual request can get cut off before the labeler ever reads it, and it guesses wrong. See Section 4 for the mechanism.


3. Where this came from — brief history

One engineer (Gera) built and has owned this almost entirely since it started, June 13, 2026. It’s been through several redesigns as real data revealed problems:


4. Stress-test findings — what reliably breaks it

We ran two separate tests against the real labeler (not a simulation):

Test A — does it agree with itself? We took 25 real conversations, re-ran them through the labeler a second time, and compared the fresh answer to what’s stored. 98.1% of individual messages matched. Every disagreement was on a low-content connector line — Clara’s own “let me transfer you” hand-off, a bare “hi, how can I help,” a system log line — never on something a caller actually asked. That’s a strong, reassuring result: the labeler is stable and repeatable on real content.

Test B — deliberately tricky inputs. We took six clean examples (one per topic) and hit each with four kinds of distortion:

Distortion Broke it?
Spanish translation No — 0 of 6
Trimmed to a short, curt exchange No — 0 of 6
Wrapped in AppFolio/Zillow-style automated-notification boilerplate Yes — 1 of 6
Real request buried behind ~220+ characters of filler text Yes — 1 of 6

Both breaks trace to the same root cause and the same case: the labeler only reads the first roughly 220 characters of each message before sending it to the AI model — a deliberate cost-saving choice, since reading the whole message for every one of thousands of messages would be expensive. Most real messages get to the point fast enough that 220 characters is plenty. But a forwarded AppFolio/Zillow notification email typically starts with 250+ characters of header boilerplate (“New Message Received via Resident Portal,” a reference number, a “do not reply” notice) before the actual content begins — so the labeler never sees the real message at all, and guesses.

Bottom line: the labeler handles language and short/curt exchanges fine. Its one proven weak spot is any message where the real content sits past the first ~220 characters — which is exactly the shape of a forwarded PMS/listing-site notification email.


5. Opportunities, ranked by expected payoff vs. effort

# Fix Expected effect Effort
1 Read more of the message before deciding — or read the whole thing for email specifically. Directly fixes the one proven, reproducible weak spot (Section 4) and likely fixes the “billing vs. maintenance” boundary miss on the Spanish call too, since email/forwarded messages are the shape most likely to bury the real content. High — closes the only weak spot we could actually reproduce on demand Small — one number to change, plus a cost check since longer input costs a bit more per message
2 Add explicit rules for the two boundary confusions we found on real data: “a caller who is currently or about-to-be a resident asking about apartments is leasing, not turnover,” and “a caller from a listing/data company asking about occupancy or pricing for their own database is vendor, not leasing.” Medium — directly fixes 6 of the 20 real mistakes (30%) Small — a few sentences added to the instructions the labeler follows, no code change
3 Tighten the “no topic” rules so leftover menu words and dead air don’t leak into a real topic. Medium — fixes 8 of the 20 real mistakes (40%), the single largest real-customer pattern Small-medium — needs a couple of new test cases to lock the fix in place so it doesn’t regress
4 Turn the visibility decision from July 29 back on for at least the “unrelated” bucket, so a real conversation that got miscategorized as noise doesn’t disappear from the default view. This is a decision to revisit, not a code fix. Medium — safety net against exactly the failure this audit is measuring Small — a one-line default change, but needs a product decision first (see Section 3)
5 Add a one-time backfill pass for any real (non-test) conversations that predate the labeler, if any exist beyond what this audit found. This audit found zero on the live customer, so this is precautionary, not urgent. Low right now (nothing found), but protects against a blind spot as new properties launch Medium — needs the guarded backfill process the team already has, run carefully with a spend limit
6 Let a conversation carry more than one topic more reliably for cases like “renewal call that also raised a maintenance issue.” Low-medium — didn’t show up in the real-customer mistakes, but common in the wider dataset and likely happening quietly Medium — this is closer to a design change than a quick fix
7 Move to a stronger AI model or add a second check for uncertain calls. Low — the current model already agrees with itself 98% of the time on real content; a stronger model wouldn’t fix truncation or missing rules, which are the real causes here Medium-large, and adds ongoing cost for a small expected gain — not worth doing before #1–#3

6. What to fix first

Read the whole message before deciding, for email specifically (Fix #1 above). It’s the smallest, cheapest change on the list, it’s the only failure mode we proved with a live, repeatable test rather than inferred from a sample, and email is the message type most exposed to it — Camellia’s mailbox runs through a system that regularly forwards messages with exactly this kind of header boilerplate up front. Everything else on the list is either already small in real-world impact (the boundary-confusion fixes affect single-digit real cases each) or is a product/process decision rather than a labeler fix.


Full per-conversation judge results: verdicts-batch-*.json in this folder. Full stress-test data and logs: adversarial-report.txt, adversarial-results.json, stability-results.json in this folder.

Addendum — benchmark results (same night)

A 500-case stratified benchmark now gates the labeler (PR #5913): 780 gold-labeled Camellia conversations, a judge-labeled sample of the mailbox's full 8-year history (33,725 emails extracted), and 102 adversarial synthetics. Honest numbers after a review round fixed the benchmark itself (channel budget bug, fake majority votes, parse-fail laundering):

RunAccuracyvs 99% floor
Old classifier (baseline)83.4%FAIL
After phases 1+2, full pipeline (corrected)87.8%FAIL

Phase 1's email-budget fix delivered +4.4pp overall; mailbox history 69.8% → 84.5%. Remaining named miss patterns queued for the next hardening round: "tour" fired when a showing is merely offered; out-of-office replies inheriting the bounced message's topic; the notification-wrapper pre-rule occasionally stamping a wrapped billing email as leasing; escalation flipping some correctly-quiet threads (no-topic 97.5% → 94.5%). Open framing decision for Fede: whether the 99%/99.9% bar is measured on current live traffic (≈94% today) or the full historical mix (87.8%).

Produced by a 31-agent audit workflow (judge pass over all 2,698 prod conversations + adversarial replay harness against the live labeler). Related: application-link incident (decision D5: significant hardening).

PropFlow Docs