Korely

A travel assistant that knows your taste

A user opens your travel assistant and types "find me somewhere to eat near the hotel tonight." A stateless assistant has to ask: window or aisle preferences aside, are you vegetarian? what's your budget? do you hate loud rooms? You've answered every one of those questions before, across a dozen sessions. Re-asking is the tax a memoryless agent charges on every turn.

With Korely in the loop, the assistant opens already knowing the user prefers boutique hotels over chains, eats pescatarian, caps dinner around €60, and can't do red-eye flights. It tailors the suggestion to what they like, what they can't do, and what they dislike, without re-asking. This cookbook walks the pattern at the call level, and the key call is get_profile.

The snippets use the Python SDK (pip install korely-memory). The same calls work over the REST API and the Node SDK.

The moat: the profile is the user's current taste

get_profile returns the assembled, active profile of one end user, every fact that is true now, grouped by predicate family (preferences, dietary, budget, constraints). When the user says "I'm not vegetarian anymore, I eat fish now," that new fact supersedes the old one: the two-stage contradiction check invalidates "is vegetarian" and the profile serves "is pescatarian" instead. You personalize from resolved truth, not a raw pile of history where both the old and the new preference sit side by side and the model has to reconcile them on every turn.

The scoping: one end user, their whole taste

Each traveller is one user_id. Everything the assistant learns about them, across web, app, and any channel, accumulates under that id, and get_profile reads it back per end user. (agent_id narrows to a specific agent's namespace if you run more than one.)

from korely_memory import Korely
korely = Korely(api_key="kor_live_...", region="eu")
USER = "traveller-4812" # one user_id per end user

1. Before you suggest anything: load the profile

Read the traveller's current taste in one call. get_profile is deterministic SQL retrieval, no model on the read path, and it returns the active facts grouped by family, the end user's own facts first.

profile = korely.get_profile(user_id=USER)
print(profile.total) # 9
print(list(profile.by_family)) # ["preferences", "dietary", "budget", "constraints"]
# Pull just the dietary facts straight from the grouped view
for fact in profile.by_family["dietary"]:
print(fact.subject, fact.predicate, fact.object)
# traveller-4812 is_diet "pescatarian"
{
"user_id": "traveller-4812",
"as_of": null,
"total": 9,
"truncated": false,
"by_family": {
"preferences": [
{
"id": "fact_8a1c",
"subject": "traveller-4812",
"predicate": "prefers",
"object": "boutique hotels over chains",
"predicate_family": "preferences",
"subject_type": "person",
"object_is_literal": true,
"confidence": 0.93,
"user_id": "traveller-4812",
"agent_id": null,
"valid_from": "2026-02-11",
"invalid_at": null,
"invalidated_by": null,
"source_memory_id": "mem_7c1a",
"created_at": "2026-02-11T09:14:02Z"
},
{
"id": "fact_4d20",
"subject": "traveller-4812",
"predicate": "dislikes",
"object": "loud, crowded restaurants",
"predicate_family": "preferences",
"subject_type": "person",
"object_is_literal": true,
"confidence": 0.88,
"user_id": "traveller-4812",
"agent_id": null,
"valid_from": "2026-03-02",
"invalid_at": null,
"invalidated_by": null,
"source_memory_id": "mem_2b40",
"created_at": "2026-03-02T18:41:55Z"
}
],
"dietary": [
{
"id": "fact_9f08",
"subject": "traveller-4812",
"predicate": "is_diet",
"object": "pescatarian",
"predicate_family": "dietary",
"subject_type": "person",
"object_is_literal": true,
"confidence": 0.95,
"user_id": "traveller-4812",
"agent_id": null,
"valid_from": "2026-06-09",
"invalid_at": null,
"invalidated_by": null,
"source_memory_id": "mem_c331",
"created_at": "2026-06-09T20:03:11Z"
}
],
"budget": [
{
"id": "fact_1e77",
"subject": "traveller-4812",
"predicate": "budget_cap",
"object": "EUR 60 per dinner",
"predicate_family": "budget",
"subject_type": "person",
"object_is_literal": true,
"confidence": 0.91,
"user_id": "traveller-4812",
"agent_id": null,
"valid_from": "2026-04-18",
"invalid_at": null,
"invalidated_by": null,
"source_memory_id": "mem_5a92",
"created_at": "2026-04-18T12:27:40Z"
}
],
"constraints": [
{
"id": "fact_3b55",
"subject": "traveller-4812",
"predicate": "cannot_do",
"object": "red-eye flights",
"predicate_family": "constraints",
"subject_type": "person",
"object_is_literal": true,
"confidence": 0.9,
"user_id": "traveller-4812",
"agent_id": null,
"valid_from": "2026-01-30",
"invalid_at": null,
"invalidated_by": null,
"source_memory_id": "mem_0d14",
"created_at": "2026-01-30T07:55:09Z"
}
]
}
}

Drop these facts into your system prompt and the very first suggestion lands inside the traveller's taste: a quiet pescatarian-friendly spot near the hotel under €60. No questionnaire.

New turn

User asks for a dinner suggestion

  • "Somewhere to eat near the hotel tonight"
  • Resolves to user_id "traveller-4812"

Korely

get_profile(user_id=...)

  • Active facts only, grouped by family
  • Contradictions already resolved

Suggestion

Tailored on the first try

  • Pescatarian, quiet, under EUR 60
  • No re-asking the known preferences

2. When the user states a new preference: write it down

Mid-trip the traveller mentions "actually I love rooftop bars." Capture it with add. Server-side extraction turns the sentence into a typed (subject, predicate, object) triple under the preferences family, so it shows up in the next get_profile.

korely.add(
"I love rooftop bars with a view — that's always a win for me.",
user_id=USER,
)
# Returns immediately. The typed fact
# (traveller-4812, likes, "rooftop bars with a view") lands shortly after.

If the new preference contradicts an older one, say the traveller swore off rooftop bars last winter, you don't write any supersede logic. The contradiction detector finds the conflicting fact (same predicate family, opposing object) and invalidates it; the profile keeps serving only what's true now.

3. For a specific trip query: pull a context block instead

When the request is narrow, "plan two days in Lisbon", you don't need the whole profile. get_context assembles a prompt-ready block scoped to the query: the relevant slice of the profile plus the most relevant memories, fitted to a token budget, in one call.

ctx = korely.get_context(
query="two-day Lisbon trip: where to eat, where to stay, what to avoid",
user_id=USER,
token_budget=800,
)
# ctx.context -> a Markdown block to drop into the system prompt
# ctx.tokens -> how many tokens it used
# ctx.sources -> the memory ids it drew from
{
"context": "Known preferences:\n- prefers boutique hotels over chains\n- dislikes loud, crowded restaurants\n- is pescatarian\n- dinner budget: EUR 60\n- cannot do red-eye flights\n\nRelevant past trips:\n- Loved a quiet seafood place in Porto in March; asked to avoid touristy main squares...",
"tokens": 287,
"sources": ["mem_c331", "mem_2b40", "mem_5a92"]
}

The Lisbon plan now respects the budget, the diet, and the no-red-eye constraint without a single clarifying question, and it leans on what the traveller actually enjoyed on past trips.

Why not just keep the chat history in a vector DB?

  • Stale preferences. A transcript store holds both "is vegetarian" and "eats fish now", and you're trusting the LLM to pick the right one each turn. get_profile returns only what's valid now, the contradiction is resolved before the model sees it.
  • No grouping. Personalization wants the budget facts here, the dietary facts there. by_family hands you the profile already grouped by predicate family; a vector store hands you a ranked pile of snippets to sort yourself.
  • Token cost. Re-feeding a dozen sessions of chat is thousands of tokens every turn. A profile or a get_context block is a few hundred and grows with what's worth remembering, not with how long the user has been a customer.

Two honest caveats. get_profile is capped at 200 facts, when an end user has more, profile.truncated is true and profile.total tells you the real count, so reach for a query-scoped get_context rather than the full profile for heavy users. And korely.add extracts facts asynchronously: a stated preference becomes a typed fact a few seconds after the write returns, ready by the next get_profile, not inside the same call. Write preferences fire-and-forget during the conversation; read the assembled profile at the start of the next turn.

Where to go next

Temporal facts explains how a new preference supersedes the old one under the hood; get context is the query-scoped call from step 3; the multi-session research cookbook applies the same profile-and-context pattern to a long-running research thread.