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. This cookbook shows
the memory mechanics. It is not a medical device, and Korely makes no
claim that an application built on it meets any health-care regulation.
The regulatory review of your application is yours. Health data is
allowed in a memory when you have a lawful basis for it under Article 9
GDPR and have signed our Data Processing Agreement (write to
[email protected]); we recommend
the Europe region for it. Korely
gives you these data-control primitives: erasing one patient in one call,
facts with start and end dates, and a log of who wrote and read what
(ids and counts, never content). Memories and facts are stored on our
server in Helsinki. To extract facts, the text of each memory goes to the
language model of the project's region: gpt-oss-120b on Scaleway, in
Paris, in the eu region, or Google's Gemini API in
global, which may process it outside the EU (see
Data governance and
Security). Questions about a health deployment:
[email protected].
The scoping model in one sentence
One clinic app = one agent_id ("intake-assistant").
Every patient = one end user, that
is one user_id ("patient-marco", or better, your
internal patient identifier, so that no name sits in the id). The words
fact,
memory and
agent_id are in the
Glossary. 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). Each fact below is shortened to its main fields:
{ "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:00+00:00", "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:00+00:00", "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:00+00:00", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_91f3"} ], "total": 3}
These facts go straight into the system prompt. A fact read uses no
generative model, so it is cheap enough to run on every chat open. The
JSON above is illustrative: the extractor is a model, so predicates and
objects differ from run to run. Do not treat extracted facts as the
clinical record. They are a model's reading of text, and they can be
missing or wrong. Keep your own record as the source of truth, and show
the clinician the memory text beside each fact. The health family is one of
ten predicate families. You don't define a schema: the extractor writes
each relation as a short snake_case predicate, and a model files it under
one of nine families the first time that predicate appears for an end user
(the answer is reused for every later fact with that predicate). The tenth
family, identity, is assigned by the schema to renaming predicates such as
renamed_to and also_known_as. Typical predicates look like these
(the names are examples, not a fixed list):
| 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 |
| identity | renamed_to, also_known_as |
| 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: embeddings, entity extraction, typed-fact
extraction with contradiction checking and start and end dates for facts. On the
Cloud all of that is part of the write your plan counts, with no model
charges on top. Extraction usually finishes within about ten seconds.
Anything that fits a typed predicate becomes a fact of Marco's. The note itself is always stored and
searchable. Whether a sentence also becomes a fact is the extractor's
decision: allergies, conditions and medication usually do, and a real run
also turned "pain 3/10 (was 6/10)" into pain_level facts, the
old value closed.
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 have a start and an end date: 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 | still valid |
allergic_to | penicillin | 2026-01-12 | still valid |
diagnosed_with | rotator cuff strain (right shoulder) | 2026-01-12 | still valid |
The table is illustrative: closing the old dosage is a model's judgement,
so check the history with include_invalidated=True rather
than assuming it. In a real run of a single note ("Since 2 March 2026 he
takes naproxen 250 mg as needed, down from naproxen 500 mg twice daily"),
the new dosage was open with valid_from 2026-03-02, taken from
the text, and the old dosage was closed with a past tense. The note gave no
date for the old one, so it started on the day the note was written. Contradiction detection is two-stage and runs on every
write: a broad
candidate search over the older facts about the same subject (the same
or a related relation, the same two entities, the nearest facts by
meaning), then, only when that search finds candidates, a model judges
whether the new fact replaces one 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 was
true at the March visit?" To reconstruct what the system knew at the time,
add known_as_of: "what did the system know on March 1st?" It
leaves out the facts and corrections the store learned after that date.
Facts written from a note start on the day the note was written, unless
the text states a date or you pass timestamp= to
add(), so a date before your first write returns nothing.
march_view = korely.get_facts( user_id="patient-marco", predicate_family="health", as_of="2026-03-01",)# -> the health facts valid on that date, as the store knows them today# (takes_medication: naproxen 500 mg twice daily)
known_then = korely.get_facts( user_id="patient-marco", predicate_family="health", known_as_of="2026-03-01",)# -> the health facts as the store knew them on March 1stWho 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 erases every memory and fact this project holds
for that
user_id, in every agent namespace. Encrypted backups keep a copy for up to 35 days. Check the id you send: an id that matches nothing also answers 200, with zero counts.
curl -X DELETE https://api.korely.ai/v1/users/patient-marco/memories \ -H "Authorization: Bearer kor_live_..."
# 200 OK{"user_id": "patient-marco", "memories_deleted": 14, "facts_deleted": 6, "erasure": "permanent", "audit_id": "aud_77c2"}- Patient-facing control: give the patient a screen
that lists
get_profileand callscorrect_factorforget_fact(human in the loop): they fix a wrong dosage or forget a fact themselves. No support ticket.
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 with dates
- 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?"
- No generative model on the read
The health family, the struck-through superseded dosage, the dates: the behavior this cookbook builds on. If you're building something health-adjacent, talk to us before you ship it ([email protected]). Our data-control primitives end where your regulatory review begins. For now Korely is available as Cloud; installing it on your own machines is not available at the moment (pricing).