Korely

Concepts

Architecture: reads without a language model

Korely does its language model work when you write, never when you read. This page shows what runs on each side, and where the write model runs.

When you save a memory, a language model reads it and writes facts. When your agent reads, Korely only looks up what was already written.

Quick start

Install with pip install korely-memory (Python 3.9 or later) or npm install korely-memory (Node.js 18 or later). Then get a free API key (Quickstart) and run export KORELY_API_KEY=kor_live_.... Checked against SDK 0.1.28 (Python) and 0.1.17 (Node.js).

The code saves one memory for the end user acme-account, waits, then makes three reads that run no language model.

import time
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environment
korely.add("Dana Reyes works at Acme. Dana prefers usage-based pricing.", user_id="acme-account")
time.sleep(20) # facts usually take about ten seconds
facts = korely.get_facts(user_id="acme-account")
hits = korely.search("pricing", user_id="acme-account")
block = korely.get_context("How should I pitch the renewal?", user_id="acme-account")
print([(f.subject, f.predicate, f.object) for f in facts])
print([(round(h.score, 2), h.snippet) for h in hits])
print(block.tokens, block.sources)

Output of one run (illustrative: the facts, the score and the ids vary; ids shortened):

[('acme-account', 'works_at', 'Acme'), ('acme-account', 'prefers', 'usage-based pricing')]
[(0.31, 'Dana Reyes works at Acme. Dana prefers usage-based pricing.')]
307 ['fct_...', 'fct_...', 'mem_...']
  • The note is about Dana, so the model can treat her as the end user and use your user_id as the subject. Another run can write Dana Reyes instead.
  • 0.31 is the score, closeness in meaning from about 0 to 1. A one-word question like "pricing" can score this low and still find the right memory.
  • 307 is the size of the block in tokens. block.sources lists the ids of the facts (fct_) and the memory (mem_) in the block.
  • add() is the write: the language model ran during the wait. The three reads ran no language model.

If facts is empty, read again after a few seconds. Check that both calls use the same key and the same user_id. A memory can finish with no facts: then write the fact again as one simple sentence in the present tense.

Terms

The Glossary defines the words used across the docs. These are the ones this page relies on.

  • Read path: the calls under Reads. No language model runs on them.
  • Write path: the calls under Writes. The language model runs here.
  • Embedding model: a small model on our server that turns a text into an embedding (see Search). It writes no text, so it is not a language model.
  • Query embedding: the embedding of your question, made once per search or get_context call.

The two paths

flowchart TD
    A["add (or batch, update)<br/>POST /v1/memories"] --> B["embedding of the memory<br/>inside the call"]
    B -->|"saved: processing"| S[("store<br/>memories + embeddings<br/>facts, which form the graph")]
    B -->|then| D["extraction in the background<br/>about ten seconds<br/>language model writes facts<br/>and checks contradictions"]
    D --> S

    S --> R1
    S --> R2
    S --> R3

    subgraph READ ["READ PATH, no language model"]
      R1["get_facts, get_profile, get,<br/>get_all, history, users,<br/>list_agents<br/>database read, no model"]
      R2["search<br/>query embedding, then<br/>similarity over memories"]
      R3["get_context<br/>the same, plus<br/>the current facts"]
    end

    style READ fill:#0f172a,stroke:#334155,color:#e2e8f0
    style S fill:#1e293b,stroke:#475569,color:#e2e8f0
A write makes the embedding inside the call, then extracts facts in the background. Reads use no language model.

Reference

Reads

ReadRESTSDK Python / NodeModel?
Context blockGET /v1/contextget_context() / getContext()Embedding model only
SearchPOST /v1/memories/searchsearch() / search()Embedding model only
FactsGET /v1/factsget_facts() / getFacts()None
Profile of one end userGET /v1/profileget_profile() / getProfile()None
One memoryGET /v1/memories/{id}get() / get()None
List of memoriesGET /v1/memoriesget_all() / getAll()None
History of a memoryGET /v1/memories/{id}/historyhistory() / history()None
End usersGET /v1/usersusers() / users()None
AgentsGET /v1/agentslist_agents() / listAgents()None

Pass user_id to every read when your agent serves more than one end user. A get_context, get_facts or search call without user_id reads every end user of the project. The exception is a confined key, which reads only its own end users.

  • get_context builds the context block. First come up to 10 current facts, chosen by closeness to the question. Between two equally close facts, the one that memories stated more recently, or more often, usually comes first.
  • Memories in the block fill the rest of the token budget (default 800). The first five appear in full. From the sixth on, each is one dated line: a short summary written at extraction, or the start of the text.
  • search returns the closest memories, each with an id, a score, a snippet (the first 280 characters), user_id, agent_id and metadata.
  • get_facts filters facts in the database, by end user, entity or predicate: 50 per page by default, at most 200.
  • get_profile returns the current facts of one end user, grouped by predicate family, such as work (works_at) or preferences (prefers).
  • get reads one memory and its facts that are true now. history is the timeline of one memory: created, updated, deleted, each fact extracted from it, and when a fact was closed.
  • users lists the end users with memories you have not forgotten, or with current facts.
  • list_agents lists the agent_id names used in the project. A name whose data was all forgotten stays listed, with zero counts, until you delete it.

No read sends anything to a language model provider. Each read in the table counts one read in your plan's monthly quota. On every plan the read quota is at least a hundred times the write quota: see Pricing.

Writes

Five calls use a language model:

CallRESTWhat it does
add()POST /v1/memoriesSaves one memory.
batch()POST /v1/batchSaves many memories in one call.
update()PATCH /v1/memories/{id}Rewrites the text of a memory.
add_fact_triple()POST /v1/factsWrites one fact you give, without a memory.
correct_fact()PATCH /v1/facts/{id}Replaces a wrong fact with the right one.

add, batch and update run the three steps below. The two fact calls run only step 3.

  1. Embedding. Inside the call, the embedding model turns the start of the text, up to 8,000 characters, into an embedding. Search matches on that part. Then add() returns status processing.
  2. Extraction. In the background, usually in about ten seconds, a language model reads the whole text and writes facts. A long text is read in pieces of up to 6,000 characters, one model call each. Each subject gets a type: person, organization, product, place, concept, event, or unknown. The model also writes a short summary sentence about the end user, when the memory says something about them.
  3. Contradiction check. The database finds earlier facts that could clash, then a language model decides. A contradicted fact is closed, not deleted, unless you erase it. So a read never has to choose between an old fact and a new one.

Steps 2 and 3 take several model calls per memory. They are included in your write count, with no surprise model charges. A memory counts one write per 6,000 characters, rounded up.

Where it runs

On the Cloud, the API, the database, the embeddings and the embedding model run on Korely's servers in Helsinki (Hetzner).

The write language model depends on the project's region:

  • global (the default): a model from Google (the Gemini API), outside the EU: in the United States and other countries where Google has facilities.
  • eu: gpt-oss-120b on Scaleway, in Paris.

The model receives the text of the memory and the facts to compare. Our Security page gives the details. Subprocessors are the outside companies that process data for us; the list of subprocessors names them.

Details

  • A busy server. Under a burst from one account, a read waits its turn at the embedder, up to 20 seconds. Then search answers 503 search_unavailable, and get_context answers 200 with degraded: true: the block is incomplete: the latest facts, no memories. Retry after a few seconds. Over REST a degraded get_context still counts as one read and a search ending in 503 counts none. Through MCP, search and an empty degraded block are a tool error (search_unavailable) that counts none.

See also