Skip to main content

cortadel

Official Python SDK for Cortadel — self-hosted long-term temporal graph memory for AI agents. A thin, typed client over the Cortadel REST API.

pip install cortadel
# or: uv add cortadel / poetry add cortadel

Python ≥ 3.10. Ships two facades: an async client and a real blocking client — pick whichever fits your program.

Quickstart (async)

import asyncio
from cortadel import ChatMessage, CortadelClient, SearchOptions

async def main() -> None:
    # "alice" is the user_id. It is optional against a server with auth enabled — the API key
    # already says who you are — but required here, because a local auth-disabled server has
    # nothing else to scope memories by. See "User scoping (user_id)" below.
    async with CortadelClient("http://localhost:3001", "alice") as cortadel:
        # apiKey: pass api_key="<token>" — omit when the server runs with auth disabled

        # Store
        await cortadel.add("Alice prefers dark mode and ships on Fridays.")

        # Recall (hybrid BM25 + vector + RRF)
        hits = await cortadel.search("what are alice's preferences?", SearchOptions(top_k=5))
        for h in hits.results:
            print(h.rrf_score, h.content)

        # Ingest a conversation
        await cortadel.add_conversation([
            ChatMessage(role="user", content="I'm allergic to peanuts."),
            ChatMessage(role="assistant", content="Noted — I'll avoid peanut recipes."),
        ])

        # List / get / delete
        page = await cortadel.list()
        one = await cortadel.get(page.items[0].id)
        await cortadel.delete([page.items[0].id])

asyncio.run(main())

Quickstart (blocking)

from cortadel import SyncCortadelClient

with SyncCortadelClient("http://localhost:3001", "alice") as cortadel:
    cortadel.add("Alice prefers dark mode and ships on Fridays.")
    hits = cortadel.search("what are alice's preferences?")
    for h in hits.results:
        print(h.rrf_score, h.content)

SyncCortadelClient is a real blocking client, not asyncio.run(...) called once per method — see cortadel/sync_client.py's module docstring for why that distinction matters (connection-pool reuse, and correctness when called from a thread that already has a running event loop) and how it is implemented (one persistent background event loop, for the client's lifetime).

Auth

Pass api_key in either constructor and every request carries Authorization: Bearer <key>. Omit it (or leave it None) when the server runs with auth disabled — no header is sent, and the client never mutates an httpx.AsyncClient you bring in either way.

import os
from cortadel import CortadelClient

cortadel = CortadelClient("https://my-box:3001", "alice", api_key=os.environ.get("CORTADEL_API_KEY"))

Reuse a single client per base URL + user.

User scoping (user_id)

user_id is the second constructor argument on both clients, and it is optional:

# Auth enabled — the key identifies the user; the SDK sends no user_id at all.
cortadel = CortadelClient("https://my-box:3001", api_key=os.environ["CORTADEL_API_KEY"])

# Auth disabled — no key exists, so user_id is the only thing selecting a namespace.
cortadel = CortadelClient("http://localhost:3001", "alice")
You pass What goes on the wire
nothing No user_id at all — not as a body field, not as a query parameter. The server resolves the user from the API key.
"alice" user_id is sent on every call, exactly as before.
"" / " " ValueError, raised from the constructor. Omission is the supported way to let the server decide; a blank string is a bug.

Passing a user_id is not deprecated. On an authenticated server it is redundant — the key decides the namespace, so a user_id in a request body is silently rewritten to the key's user and one in a query string is rejected with 403. On an auth-disabled server there is no key, and user_id is the only thing that scopes anything: it is still required there.

Server requirement

Omitting user_id requires a server that includes commit 30b70ea4 — the change that made the API fill in a missing user_id from the caller's key. Check which build you're talking to:

curl -s http://localhost:3001/api/health | jq -r .version
# 1.0.0+44be8adfc376d19cf6999a379cc8519331def7e6
#       ^ build metadata after the "+" is the commit SHA of the running build

Against an older server, a request that omits user_id comes back as 400 {"errors":{"UserId":["The UserId field is required."]}} — surfaced as a CortadelError with status == 400. Pass user_id explicitly to talk to those builds.

Methods

Both clients expose the same seven methods (async def on CortadelClient, blocking on SyncCortadelClient):

Method Returns Notes
add(text, options=None) MemoryCreated Store a memory. options.infer (default True) runs background entity/category extraction; False stores verbatim (dedup still applies).
add_conversation(messages, options=None) ConversationResult Distill atomic facts from a transcript and store each one.
search(query, options=None) SearchResults Hybrid search (BM25 + vector fused with RRF); set options.rerank = "cross_encoder" to rerank.
list(options=None) MemoryList Paginated, newest-first. options.size defaults to 20 (a deliberate SDK-wide choice — see below).
get(memory_id) MemoryDetail | None None when the memory doesn't exist; never raises for a 404. Content field is .text.
delete(memory_ids) str Deletes one or more memories; returns the server's confirmation message.
health() HealthResult Database + embedding provider reachability. Does not raise when the server reports itself degraded — a degraded server is a normal return value (status == "degraded"), not an exception.

ListOptions.size defaults to 20, not the REST contract's own default of 10 — kept in sync with the .NET and TypeScript SDKs so every Cortadel SDK behaves identically regardless of which one you're reading examples for.

Errors

Any non-success response — other than a degraded health check, which health() returns instead of raising — raises a CortadelError:

from cortadel import CortadelClient, CortadelError

async with CortadelClient("http://localhost:3001", "alice") as cortadel:
    try:
        await cortadel.add("")
    except CortadelError as err:
        print(err.status, err.code, err.message)
    # asyncio.CancelledError from a cancelled task/timeout propagates untouched instead —
    # it is never wrapped as a CortadelError.
Attribute Meaning
status HTTP status code. 0 when the transport failed before a status was known.
code Machine-readable error code (e.g. not_found, validation_error).
message Human-readable message — for a 400 from model validation, this folds in the
server's per-field errors instead of a generic "the request failed".

Bring your own HTTP client

Both constructors accept http_client: httpx.AsyncClient to reuse an existing client (connection pooling, proxies, custom TLS, etc. all carry over). It is never mutated or closed by the SDK — you own its lifecycle either way.

import httpx
from cortadel import CortadelClient

async with httpx.AsyncClient(proxy="http://proxy.internal:8080") as http_client:
    cortadel = CortadelClient("http://localhost:3001", "alice", http_client=http_client)
    ...

License

Apache-2.0

Metadata

Release files for cortadel 1.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cortadel 1.2.0
File Size Uploaded
cortadel-1.2.0.tar.gz 34.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cortadel 1.2.0
File Interpreter ABI Platform
cortadel-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 98.9 kB

Release files / cortadel-1.2.0.tar.gz

Download URL cortadel-1.2.0.tar.gz
Size 34.3 kB
Tags Source
SHA-256 checksum
How to use checksums
4c7bc7b838fb40f4f5bf3809750160c988afe6e60564635b7fc46e2c2387bc5c
BLAKE2b-256 checksum
How to use checksums
2456516550d5f24263aa2848ea04bcedd5a9d37d08c564e53fbec95dff1ab840
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / cortadel-1.2.0-py3-none-any.whl

Download URL cortadel-1.2.0-py3-none-any.whl
Size 64.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f00b4d419e48a4b727dab20f6d1085ca09327de73da1385c36800624ec2e7edb
BLAKE2b-256 checksum
How to use checksums
2b086b5eccb0716427490fdc53059f1b620c9bb6bd005706b132b9452283a11f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page