A patient assistant that never forgets the chart
Picture a physiotherapy clinic with an intake assistant in the booking flow. Patient Marco opens the chat before his fourth visit. With a stateless bot, Marco gets the same intake questionnaire every single time: "Any allergies? Any current medication? Where does it hurt?" He answered all of this in January, February, and March. With a Korely-backed bot, the assistant opens with what it already knows: the rotator cuff strain from January, the penicillin allergy, the medication change in March, the session note that he responded well to exercise B. The chat starts where the last visit ended, because the developer wired in the four calls on this page.
Read this before anything else. If you're deploying in a regulated health context, talk to us first at [email protected]. We handle health deployments case-by-case, including self-host and on-prem options. Korely gives you the data-control primitives: per-patient deletion in one call, a bi-temporal audit trail, and all cloud data EU-hosted on our own infrastructure. The regulatory review of your application is yours. This cookbook shows the memory mechanics. It is not a turnkey medical device.
The scoping model in one sentence
One clinic app = one agent_id ("intake-assistant").
Every patient = one user_id ("patient-marco", or
better, your internal patient identifier). End users are
unlimited on every tier. 10,000 patients is still one
agent_id with 10,000 user_id values. Filters
are additive: both together return only what this app knows about this
patient.
Call 1: intake opens, load the chart context
When Marco opens the chat, before the LLM says a word, your backend pulls his health facts:
from korely_memory import Korely
korely = Korely(api_key="kor_live_...")
facts = korely.get_facts( user_id="patient-marco", predicate_family="health",)
The response is a flat list of the typed facts Korely extracted from
prior sessions. A fact is live when invalid_at is
null; superseded facts carry the date they were invalidated
(see Call 3):
{ "facts": [ {"id": "fct_19ce", "subject": "patient-marco", "predicate": "allergic_to", "predicate_raw": "allergic_to", "object": "penicillin", "predicate_family": "health", "confidence": 0.95, "valid_from": "2026-01-12T09:30:00Z", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_4a1c"}, {"id": "fct_22b0", "subject": "patient-marco", "predicate": "diagnosed_with", "predicate_raw": "diagnosed_with", "object": "rotator cuff strain (right shoulder)", "predicate_family": "health", "confidence": 0.92, "valid_from": "2026-01-12T09:30:00Z", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_4a1c"}, {"id": "fct_4f81", "subject": "patient-marco", "predicate": "takes_medication", "predicate_raw": "takes_medication", "object": "naproxen 250 mg as needed", "predicate_family": "health", "confidence": 0.9, "valid_from": "2026-03-02T10:05:00Z", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_91f3"} ], "total": 3}
These facts go straight into the system prompt. Reads from the fact
store are deterministic SQL lookups, no model calls, typically under
50 ms even with thousands of facts in the store. Cheap enough to
run on every single chat open. The health family is one of
nine predicate families Korely extracts natively. You don't define a
schema, the typed predicates already exist:
| Family | Example predicates |
|---|---|
| preferences | likes, dislikes, prefers |
| people | knows, married_to, works_with |
| places | lives_in, visited |
| work | works_at, role_is |
| ownership | owns, uses |
| health | allergic_to, diagnosed_with, takes_medication |
| financial | pays_for, subscribed_to |
| events | attended, scheduled |
| other | catch-all typed edges |
Call 2: during the session, write the note
After the visit, the assistant (or the therapist's dictation flow) stores what happened:
memory = korely.add( content="Responded well to exercise B, pain 3/10 (was 6/10)", user_id="patient-marco", agent_id="intake-assistant",)The raw note is stored and searchable as-is. The write path is where the intelligence runs: document and chunk embeddings, entity extraction on our own infrastructure, typed-fact extraction with contradiction checking and bi-temporal validity. About a tenth of a cent per memory, all included. Anything that fits a typed predicate is added to Marco's graph. Unstructured observations ("responded well to exercise B") stay retrievable via semantic search; structured ones (allergies, diagnoses, medication) become the facts Call 1 returns.
Call 3: the treatment history, never silently lost
In March, Marco's dosage changed. A naive store would overwrite the old value, and a clinic cannot afford "we don't know what he was taking in February." Korely facts are bi-temporal: a contradiction doesn't delete the old fact, it invalidates it with a date. Pull the full chain:
history = korely.get_facts( user_id="patient-marco", include_invalidated=True,)| Predicate | Value | Valid from | Invalidated |
|---|---|---|---|
takes_medication | 2026-01-12 | 2026-03-02 | |
takes_medication | naproxen 250 mg as needed | 2026-03-02 | — |
allergic_to | penicillin | 2026-01-12 | — |
diagnosed_with | rotator cuff strain (right shoulder) | 2026-01-12 | — |
Contradiction detection is two-stage and runs on every write: a cheap candidate match on subject and predicate, then a focused conflict check before anything is invalidated. By the time your agent reads, the conflict is already resolved. Temporal facts covers the mechanism in depth.
Point-in-time queries
Because every fact carries valid_from and
invalid_at, the API exposes as_of: "what did
we know at the March visit?" For anything resembling a clinical audit
trail, this is the difference between "the database says X today" and
"we can prove what the system knew on March 1st."
march_view = korely.get_facts( user_id="patient-marco", predicate_family="health", as_of="2026-03-01",)# -> takes_medication: naproxen 500 mg twice daily# (the fact that was valid on that date, audit-grade)Who controls the data
Two deployment shapes, two control models:
- Clinic scenario (this cookbook): control stays API-side, in your hands. A patient asks you to erase everything about them? One call wipes everything this app knows about them. Per-patient, bulk, done:
curl -X DELETE https://api.korely.ai/v1/users/patient-marco/memories \ -H "Authorization: Bearer kor_live_..."
# 200 OK{"user_id": "patient-marco", "memories_forgotten": 14, "facts_invalidated": 6, "audit_id": "aud_77c2"}- Personal memory store scenario: if the patient uses Korely directly as their own account, they see and control every fact through the Memory Panel: edit a wrong dosage, forget a fact entirely. No support ticket, no developer in the loop.
The whole loop
Visit 1: January
Intake session
- korely.add("Responded well to exercise B...")
- user_id: patient-marco
- agent_id: intake-assistant
Korely
Typed facts, bi-temporal
- allergic_to: penicillin
- diagnosed_with: rotator cuff strain
- takes_medication: 500 mg → 250 mg (chain kept)
Visit 2, and every visit after
Greeting with context
- get_facts(predicate_family="health") on chat open
- "Welcome back Marco. How is the shoulder since we switched to exercise B?"
- Deterministic read, typically under 50 ms
The health family, the struck-through superseded dosage, the dates: the exact behavior this cookbook builds on. If you're building something health-adjacent, talk to us before you ship it ([email protected]). We are honest about where our data-control primitives end and your regulatory review begins, and the self-host conversation is shorter than you'd expect.