Korely

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):

FamilyExample predicates
preferenceslikes, dislikes, prefers
peopleknows, married_to, works_with
placeslives_in, visited
workworks_at, role_is
ownershipowns, uses
healthallergic_to, diagnosed_with, takes_medication
financialpays_for, subscribed_to
eventsattended, scheduled
identityrenamed_to, also_known_as
othercatch-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,
)
PredicateValueValid fromInvalidated
takes_medication naproxen 500 mg twice daily 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 1st

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 project holds about them, in every agent namespace. Per-patient, bulk, done:
Terminal window
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_profile and calls correct_fact or forget_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.