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
info@korely.ai. 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
memories and facts stored in the EU on our own infrastructure (to
extract facts, the text of each memory goes to the language model of the
project's region:
Google's Gemini API (United States and other countries where Google
has facilities), by default, or gpt-oss-120b
on Scaleway, in Paris, in the eu region; see
Security).
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). 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. Reads from the fact
store are deterministic SQL lookups, no model calls, however many facts
are in the store. Cheap enough to
run on every single chat open. 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 (the answer is
stored and 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 bi-temporal validity. On the Cloud all of that is part of the write your plan counts, with no model charges on top. 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 | still valid |
allergic_to | penicillin | 2026-01-12 | still valid |
diagnosed_with | rotator cuff strain (right shoulder) | 2026-01-12 | still valid |
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?" For anything resembling a clinical audit
trail, 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.
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 wipes everything this project holds about them, in every agent namespace. 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_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, 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, no model call
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 (info@korely.ai). 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.