Skip to main content

Tex Python SDK

SuperMemory-compatible memory SDK for Python.

Installation

pip install tex-sdk

Optional HTTP/2 support:

pip install "tex-sdk[http2]"

Requires Python 3.9+.

Quick Start

API key

from tex import Tex

client = Tex(api_key="sk-...", base_url="http://localhost:8000")

client.add("The quick brown fox jumps over the lazy dog.")
results = client.search.documents("fox")

Environment variables

Set TEX_API_KEY and TEX_BASE_URL, then:

from tex import Tex

client = Tex()

Per-user clients (production)

To act as one end user of your app, pass that user's id alongside your API key. The SDK makes a single POST /auth/token-exchange with {api_key, user_id, session_id} and receives a JWT minted for that user:

import os
from tex import Tex

# base_url falls back to TEX_BASE_URL, api_key to TEX_API_KEY
alice = Tex(
    api_key=os.environ["TEX_API_KEY"],
    base_url=os.environ["TEX_BASE_URL"],
    org_id="org1",          # optional; if set, must match the key's org
    user_id="alice",
    session_id="chat-42",   # optional
)
alice.recall(q="what did I order last week?", session_id="chat-42")  # every call is scoped to alice
  • The API key must carry an impersonation-capable scope, normally impersonate_user (also accepted: tenant:write:any_user, agent, service, system, *). Without one the backend refuses the user_id and the SDK raises PermissionDeniedError saying so.
  • The minted token carries an act claim naming the key: it is locked to that user and cannot impersonate anyone else. The SDK also checks the returned token is really for the requested user.
  • On expiry the SDK uses the refresh token, and otherwise re-exchanges for the same user_id.
  • The per-user token is sent on every call, including the Supermemory-compatible ones (memories.add, documents.add, search.memories, profile, ...), so they also act as that user with that user's team scopes. container_tag on such a client must be omitted or equal the user id; naming another user is refused with 403 container_tag_not_permitted. (Requires a backend that accepts per-user tokens on /v3 and /v4.)
  • Omit user_id to act as the key's own identity; compat calls then send the raw API key and container_tag selects the end user, Supermemory-style.

Org + user login (development servers only)

from tex import Tex

client = Tex(base_url="http://localhost:8000", org_id="org1", user_id="user1")

With no API key the SDK calls POST /auth/login, which exists only when the backend runs with DEBUG=true. Production returns 403 and the SDK raises PermissionDeniedError pointing you to the per-user API key pattern above.

Documents

Add, retrieve, update, delete, list, and batch-manage documents.

# Top-level shortcut
resp = client.add("Document content", container_tag="notes")

# Equivalent resource call
resp = client.documents.add("Document content", container_tag="notes")

# Get by ID
doc = client.documents.get("doc_abc123")

# Update
client.documents.update("doc_abc123", content="Updated content")

# Delete
client.documents.delete("doc_abc123")

# List with filters
docs = client.documents.list(container_tags=["notes"], limit=20)

# Batch add
client.documents.batch_add(
    ["First document", "Second document"],
    container_tag="batch-import",
)

# Bulk delete
client.documents.delete_bulk(container_tags=["old-tag"])
results = client.search.documents("quarterly revenue", limit=10)

# Alias
results = client.search.execute("quarterly revenue", limit=10)
results = client.search.memories("user preferences", container_tag="user1")

Memories

# Add memories
client.memories.add(
    [{"content": "User prefers dark mode"}],
    container_tag="prefs",
)

# Forget a memory
client.memories.forget("prefs", id="mem_abc123")

# Update a memory
client.memories.update_memory("prefs", "User prefers light mode", id="mem_abc123")

Profile

profile = client.profile("user-container-tag")
print(profile.profile)

Tex-Unique Features

NLQ (Natural Language Query)

result = client.nlq.execute("What meetings did I have last week?")
print(result.text)
print(result.evidence)

Episodes

# Store a conversation
client.episodes.store(
    [
        {"role": "user", "content": "Book a table for two"},
        {"role": "assistant", "content": "Which restaurant?"},
    ],
    episode_id="ep_001",
)

# List episodes
episodes = client.episodes.list(limit=10)

Preferences

client.preferences.store([
    {"key": "drink", "value": "espresso", "confidence": 0.9},
])

DB

result = client.db.query("MATCH (n) RETURN n LIMIT $limit", params={"limit": 5})
schema = client.db.schema()

Auth

info = client.auth.whoami()

Jobs

status = client.jobs.get("job_abc123")

Scope memberships

A user always sees their own user:<id> scope and their org:<org> scope — those are implicit. Every shared team: or project: space is an explicit grant, and client.scopes lets your own org admin manage those grants instead of asking us to.

Same key as deletion: a non-impersonated org-admin API key (role admin or *, on a client built without user_id=). A per-user token inherits the key's roles and is refused, so it can never grant itself a scope. The org is always the token's — cross-org grants are impossible here.

grant = client.scopes.grant("alice", "team:eng")          # role="member" by default
print(grant.created)            # False on an idempotent replay
print(grant.membership.role, grant.membership.created_at)
print(grant.note)               # the propagation caveat, see below

listing = client.scopes.list(scope="team:eng")            # both filters optional
print(listing.total)
for m in listing.memberships:
    print(m.user_id, m.role)

client.scopes.revoke("alice", "team:eng")                 # NotFoundError if there is no such grant
  • A change is not instant. A user's read-set is baked into their helix token when it is minted, so a grant or revocation reaches them on their next token refresh — up to 1 hour. Already-issued tokens keep their old read-set until they expire: a fresh grant is not immediately visible, and a revoked scope is not immediately unreadable. When the data itself must go, pair the revocation with a deletion.
  • Only team: and project: scopes can be granted. user: is personal and implicit for its own user (granting someone else's would hand over their private memory) and org: is already implicit for every member. Both raise ScopeNotDelegableError before any request; a malformed scope id raises InvalidScopeError.
  • Granting to a user who has not logged in yet is fine. A user is just a string in a token, so the row sits inert until someone authenticates as that user in your org — onboarding before first login is a normal flow, not a workaround.
  • Replays are idempotent. Re-granting returns the existing row with created=False; passing a different role updates it in place.
  • Implicit scopes are not rows, so list() never shows them — an empty result does not mean the user sees nothing.

Platform operators have the same three calls across orgs at client.admin.scopes.* (explicit org_id=, any scope kind); a normal customer key gets 403 there.

Deletion

client.deletions erases an org's data across every tier in one request and returns an auditable receipt. It needs a non-impersonated org-admin API key — role admin (or *), on a client built without user_id=. A per-user token inherits the key's roles but is still refused (403).

receipt = client.deletions.delete_user_scope("alice", reason="GDPR erasure request")

print(receipt.status)          # "completed" | "partial"   (also .completed / .partial)
print(receipt.purge_after)     # when the retention window elapses
print(receipt.tiers.helixdb.deleted, receipt.tiers.helixdb.refused)
print(receipt.tiers.active_memory.fragments_deleted)
print(receipt.tiers.auth_db.usage_events_retained)

for gap in receipt.not_covered:
    print("not covered:", gap.tier, "—", gap.reason)

# Re-read the receipt later
receipt = client.deletions.get(receipt.request_id)

Targets:

client.deletions.delete_user_scope("alice")                     # one user's personal scope (grants kept)
client.deletions.delete_scope("team:eng")                       # everything stamped with a scope
client.deletions.delete_user("alice")                           # a user: personal data, active memory, grants
client.deletions.delete_memory("node-7", owner_user_id="alice") # one memory node; owner is REQUIRED
client.deletions.create(kind="memory", value="node-7", owner_user_id="alice")  # general form

delete_memory requires owner_user_id — the id of the user who owns the node. The server does not default it to the caller; an empty value raises ValueError before any request.

delete_user removes the user's personal-scope long-term memory only: what they wrote into team: / project: scopes stays (use delete_scope for shared data). Their active-memory data is removed across all scopes and their scope memberships are removed; usage/billing and episode-registry rows are retained in soft mode. delete_user_scope leaves scope grants alone (tex.scopes.revoke removes one).

Every method takes reason= and idempotency_key= (replaying a key returns the first receipt instead of re-running). A malformed scope id raises InvalidScopeError before any request.

What a deletion actually does

  • Soft by default, and soft is the only mode an org owner may ask for. The target is tombstoned across the graph engine, active memory and the auth DB: it disappears from every read path — recall, search, profile, NLQ — immediately, and nothing can surface it again. It is not yet destroyed; receipt.purge_after says when it becomes eligible for the hard purge.
  • Purge is platform-admin only. mode="purge" on client.deletions.* raises PurgeNotPermittedError before any request. An org owner can make their data unreachable and start the clock; irreversible destruction is a platform operation, so a compromised org key cannot destroy anything on the spot.
  • Ingestion records and raw S3 files are not covered yet. The ingestion service's documents and jobs and the raw uploaded objects in object storage are untouched; a follow-up phase owns them. Every receipt lists them under not_covered.
  • The graph engine tombstones rather than reclaiming space. HelixDB exposes only a soft delete, so even a purge leaves tombstoned nodes for engine compaction (helixdb_hard_delete in not_covered). Its delete is owner-only, so nodes it refuses are counted in tiers.helixdb.refused (the receipt goes partial) rather than raising, and nodes with no scope stamp are left alone (skipped_unscoped) so a shared-scope deletion cannot erase someone's personal data.
  • Billing survives a soft deletion. Usage events are retained (tiers.auth_db.usage_events_retained) and removed only by a purge.

Platform-admin surface

client.admin.deletions.* wraps /admin/deletions. These routes are for Tex operators — a normal customer key gets 403. They take a required org_id= and may ask for mode="purge".

client.admin.deletions.delete_user("bob", org_id="org1", mode="purge")
client.admin.deletions.get("del_abc123")                # any org's receipt

# Hard-delete everything past its retention window (point a cron at this)
result = client.admin.deletions.run_purges(limit=50)
print(result.purged, len(result.receipts))

Error Handling

The SDK provides a detailed error hierarchy:

TexError
  APIError
    APIConnectionError
      APITimeoutError
    APIStatusError
      BadRequestError          (400)
      AuthenticationError      (401)
      PermissionDeniedError    (403)
      NotFoundError            (404)
      ConflictError            (409)
      UnprocessableEntityError (422)
      RateLimitError           (429)
      InternalServerError      (500+)
    APIResponseValidationError
from tex import Tex, NotFoundError, AuthenticationError, APIStatusError

client = Tex(api_key="sk-...", base_url="http://localhost:8000")

try:
    doc = client.documents.get("nonexistent")
except NotFoundError as e:
    print(f"Not found: {e}")
except AuthenticationError as e:
    print(f"Auth failed: {e}")
except APIStatusError as e:
    print(f"HTTP {e.status_code}: {e.message}")
    print(f"Request ID: {e.request_id}")

Active memory inactivity window

Conversation turns stored with conversations.remember land in short-term active memory first. An active-memory episode (one per session_id) is closed and consolidated into long-term memory once it has been idle for its inactivity window — 30 minutes by default. Tune it to the use case: a support chat can close after a few minutes, a long-running agent task may need hours.

# Per conversation (the episode keeps this window for later writes)
client.conversations.remember(turns, session_id="ticket-981", inactivity_timeout_seconds=2 * 60 * 60)

# Client-wide default for every write that does not pass its own
client = Tex(api_key="sk-...", active_inactivity_timeout_seconds=10 * 60)
  • Resolution: per-call value → client default → org default (set by an org admin with PATCH /me/settings {"active_memory_inactivity_seconds": 3600}) → 30 minutes.
  • Bounds: 300–86400 seconds (5 minutes to 24 hours). Invalid values raise InvalidInactivityTimeoutError before any request.
  • memories.add and memories.update_memory accept the same inactivity_timeout_seconds.

Configuration

Use TexConfig and TexEndpoints for advanced configuration:

from tex import Tex, TexConfig, TexEndpoints

config = TexConfig(
    base_url="https://api.example.com",
    api_key="sk-...",
    timeout_s=30.0,
    max_retries=3,
    http2=True,
    active_inactivity_timeout_seconds=3600,  # optional; see "Active memory inactivity window"
    endpoints=TexEndpoints(
        db_query="/custom/db/query",
        db_schema="/custom/db/schema",
    ),
)

client = Tex(config=config)

Default timeout is 60 seconds. Default max retries is 2.

Migration from SuperMemory

SuperMemory Tex SDK
client.add(content) client.add(content) or client.documents.add(content)
client.documents.get(id) client.documents.get(id)
client.documents.update(id) client.documents.update(id)
client.documents.delete(id) client.documents.delete(id)
client.documents.list() client.documents.list()
client.documents.batch_add(docs) client.documents.batch_add(docs)
client.search.execute(q) client.search.execute(q) or client.search.documents(q)
client.search.memories(q) client.search.memories(q)
client.memories.add(memories) client.memories.add(memories)
client.memories.forget(tag) client.memories.forget(tag)
client.profile(tag) client.profile(tag)

Tex extends SuperMemory with additional resources: nlq, episodes, preferences, db, auth, jobs, usage, scopes, and deletions (plus the platform-admin-only admin.scopes / admin.deletions).

HTTP/2

HTTP/2 is enabled by default. If the h2 package is not installed, the SDK falls back to HTTP/1.1 automatically. To install with HTTP/2 support:

pip install "tex-sdk[http2]"

To explicitly disable HTTP/2:

client = Tex(api_key="sk-...", base_url="http://localhost:8000", http2=False)

Metadata

Release files for tex-sdk 1.3.1

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

Source distribution (sdist)

Source distribution for tex-sdk 1.3.1
File Size Uploaded
tex_sdk-1.3.1.tar.gz 58.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tex-sdk 1.3.1
File Interpreter ABI Platform
tex_sdk-1.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 105.9 kB

Release files / tex_sdk-1.3.1.tar.gz

Download URL tex_sdk-1.3.1.tar.gz
Size 58.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1e8fd537483e60686af5280de380be858c03e4f0dff81fc9da8052de716917cc
BLAKE2b-256 checksum
How to use checksums
8ee8db03e0dcaab09faea940c8b131ca3f2c95d28d1bf6ac7e4ae31809f81886
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.5

Release files / tex_sdk-1.3.1-py3-none-any.whl

Download URL tex_sdk-1.3.1-py3-none-any.whl
Size 47.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
95ec0dd164160296a746fa92291c8600b895ffc0b6e65fe164e6b8ab282447ea
BLAKE2b-256 checksum
How to use checksums
8f5d0f99fea824772c1511d2d9f305b888fe175a802ea2ddd34f1a82320922f7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.5

Release history Release notifications | RSS feed

1.5.1

2 release files

1.5.0

2 release files

This release

1.3.1 This release

2 release files

1.3.0

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

0.3.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