Vity Python SDK
Vity gives your AI application long-term memory. This package is the Python client for the Maximem memory API, by Maximem AI. It has a synchronous client and an asyncio client.
from maximem_vity import VityClient
client = VityClient(channel="my-app") # reads MAXIMEM_API_KEY
client.store("User prefers dark mode", memory_type="preference")
print(client.recall("What theme does the user like?"))
Using JavaScript or TypeScript? Use @maximem/vity-sdk on npm. It has the same methods. See Python and JS differences.
Contents
- Install
- Quick start
- Configuration
- Core methods
- Async usage
- Use as an agent tool
- Errors
- Retries and timeouts
- Testing your code
- Python and JS differences
- Requirements
- Versions and links
- License
Install
pip install maximem-vity-sdk
Quick start
Create an API key at app.maximem.ai/api-keys and set it in your environment:
export MAXIMEM_API_KEY="mx_..."
A typical agent turn recalls memory before it answers, then captures the conversation afterwards:
from maximem_vity import VityClient
with VityClient(channel="support-bot") as client:
# Before answering: get memory for the prompt.
context = client.recall("Can you book my usual table?")
# After answering: send the exchange so Vity can learn from it.
client.capture(
[
{"role": "user", "content": "Can you book my usual table?"},
{"role": "assistant", "content": "Booked the window table for 7pm."},
],
session_key="conversation-8f2c",
)
# Store one fact directly.
client.store("Prefers window seats", memory_type="preference", importance="high")
# Search memory.
for hit in client.search("seating preferences", top_k=5):
print(f"{hit['score']:.2f} {hit['content']}")
Key ideas:
- Memory belongs to your Maximem account. Every API key on the account reads and writes the same memory. Adding or rotating keys loses nothing.
channelnames your integration (for example"slack"or"support-bot"). Together withsession_keyit identifies a conversation.session_keyidentifies one conversation. Use the same value for every turn of a conversation and a new value for each new conversation.
Configuration
client = VityClient(
api_key="mx_...", # default: MAXIMEM_API_KEY
channel="my-app",
agent_id="assistant",
timeout=30.0,
max_retries=2,
)
| Argument | Environment variable | Default | Description |
|---|---|---|---|
api_key |
MAXIMEM_API_KEY |
none (required) | Your Maximem API key. Raises VityAuthError if missing. |
endpoint |
MAXIMEM_ENDPOINT |
Maximem production API | Only for a non-default environment. Must start with https:// or http://. |
timeout |
30.0 |
Request timeout in seconds. Must be greater than 0. | |
channel |
"unspecified" |
The name of your integration. Set it. | |
agent_id |
"unspecified" |
A tag sent with stores and captures. capture() can override it per call. |
|
max_retries |
2 |
Retries for temporary failures. 0 turns retries off. Keyword-only. |
|
http_client |
created for you | Your own httpx.Client (httpx.AsyncClient for AsyncVityClient). Keyword-only. |
|
user_id |
Deprecated. Ignored, and emits a FutureWarning. Removed in 0.4.0. |
Arguments take precedence over environment variables. The default endpoint is available as maximem_vity.DEFAULT_ENDPOINT.
If you pass http_client, set the timeout on that client. The timeout argument only configures the client the SDK creates itself. The SDK does not close a client you pass in.
Use the client as a context manager (with), or call close() when you are done.
Core methods
Every method exists on VityClient and, as a coroutine, on AsyncVityClient. Arguments are checked before any request is sent. A bad argument raises VityInvalidInputError.
| Method | Purpose |
|---|---|
recall |
Get memory as text to put in a prompt. |
live_context |
Get only the user's live work from other apps. |
capture |
Send a conversation for background extraction. |
store |
Save one fact now. |
search |
Find memories by meaning, with scores. |
forget |
Preview or delete memories. |
knowledge, knowledge_diff |
Read consolidated knowledge about the user. |
get_profile_card |
Read the user's Profile Card. |
meetings_enabled, meeting_ask, get_meeting, list_meeting_reviews |
Work with meeting memory. |
store_many, get, update, delete, list, stats |
Preview. Not yet generally available. |
Recall
client.recall(
current_prompt,
recent_messages=None,
max_tokens=1000,
strategy="hybrid",
live_context=False,
session_key="",
) -> str
recall() returns text you can put in a system prompt, or "" when nothing relevant is stored.
recent_messages: recent turns as{"role", "content"}dicts. Only the last 20 are sent.max_tokens: the size budget for the returned context. Must be 100 to 10,000.strategy:"semantic","recency"or"hybrid".live_contextandsession_key: see Live context.
Each memory line in the result starts with the day it was said, when known, for example - [2026-03-14] Prefers window seats.
Capture
client.capture(messages, agent_id="", session_key="", channel=None) -> CaptureResult
capture() sends a conversation for Vity to learn from. Use it after each turn.
messages: up to 50{"role", "content"}dicts.timestampis optional.session_key: the conversation's key. Use the same value on every turn of one conversation.agent_idandchannel: override the client's values for this call.
Capture is asynchronous:
- The call returns when the conversation is accepted. Vity extracts durable facts in the background, usually within seconds to minutes. Small talk is dropped.
- A captured turn is not searchable straight away. "Captured" does not mean "stored".
- The counts in the result (
captured,deduplicated) areNonefor an accepted capture. capture([])makes no request and returns zeros.- To send more than 50 messages, split them across calls with the same
session_key. - A capture is never retried once the request may have reached the server. This prevents the same conversation from being stored twice.
timestamp is the time the message was said, in epoch milliseconds. You can also pass epoch seconds, an ISO 8601 string or a datetime, and the SDK converts it. A value that is missing or unreadable becomes the current time. An ISO string or datetime without a time zone is read as UTC. The same rule applies to recent_messages in recall(). Your list is never modified.
capture_status(request_id) polls the processing status of an earlier capture by its request id and returns a dict.
Store and search
client.store(content, memory_type="fact", category="", importance="medium") -> StoreResult
client.search(query, top_k=10, limit=0, category="", min_score=0.0) -> list[SearchResult]
store() saves one fact immediately and returns {"id", "stored"}. It keeps exactly what you send. To extract facts from a whole conversation, use capture().
content: up to 10,000 characters.memory_type:fact,preference,emotion,episode,knowledgeorprofile. This is a shortcut that maps to a category.category: overridesmemory_type. One ofpreference,fact,task,relationship,context.importance:"low","medium"or"high".
search() finds memories by meaning. top_k is the maximum number of results (1 to 20). limit is an alias for top_k and wins when set. min_score is 0 to 1, and 0.0 means no minimum.
Each result is a dict:
| Field | Meaning |
|---|---|
content |
The memory text. |
type |
The memory category. |
score |
Relevance, 0 to 1. |
id |
The memory id. |
created_at |
When the memory was stored. |
chat_at |
When it was said, or None if unknown. |
happened_at |
chat_at when known, otherwise created_at. |
All dates are epoch milliseconds. An old chat imported today has a recent created_at and an old chat_at. Use happened_at to order memories by when things happened.
If search finds nothing, the SDK checks recall before it reports no results. Results from that fallback can have a created_at of 0, a chat_at of None and an empty id.
Forget
client.forget(query="", dry_run=True, delete_all=False) -> ForgetResult
forget() returns {"count", "ids"}. It only previews unless you pass dry_run=False.
client.forget("old employer") # preview what would be deleted
client.forget("old employer", dry_run=False) # delete matching memories
client.forget(dry_run=False, delete_all=True) # delete every memory on the account
Matching is by meaning, not exact text. One query can match several memories, including related but different facts. Check count and ids in the preview before you delete.
Deleting without a query removes every memory on the account, so it needs delete_all=True. Passing both a query and delete_all=True is an error.
Knowledge
client.knowledge(
query="", scope="chat", limit=10, min_score=0.3, include_candidates=False,
filters=None, offset=0, include=None, echo_slug=None,
) -> KnowledgeResult
client.knowledge_diff(
input_text, purpose, scope="chat", include_candidates=True, include_trace=False,
) -> KnowledgeDiffResult
knowledge() returns consolidated knowledge about the user: topics, summaries and confidence.
scope:"chat","echo"or"all". Useecho_slugto choose the echo when the scope is"echo".limitis 1 to 100 andoffsetis 0 to 10,000. Use them to page through results.min_scoreis 0 to 1.include: any of"items","facets","profile".filters: for example{"topic_category": ["work"], "confidence_min": 0.5, "seen_after": "2026-01-01"}. Other keys includetopic_subcategory,knowledge_type,expertise_level,seen_beforeandtopic_id. The SDK converts snake_case keys to the API's camelCase names.
knowledge_diff() returns the knowledge most relevant to a piece of text. purpose says what knowledge you need and why. It is a selection criterion, not a formatting instruction. A status of "degraded" is a valid partial answer.
client.knowledge_diff(
input_text="Draft: proposal to move our analytics to ClickHouse",
purpose="the user's earlier decisions and opinions about analytics databases",
)
Profile card
client.get_profile_card() -> ProfileCard
get_profile_card() returns the user's Profile Card: who they are, built from their memories and kept up to date. It is a fast read and makes no LLM call.
card = client.get_profile_card()
name = card["identity"]["name"] # {"value": "Ada", "confidence": 0.95, ...} or None
skills = [s["value"] for s in card["knowledge"]["skills"]]
Sections:
| Section | Fields |
|---|---|
identity |
name, email |
professional |
current_company, current_role, company_history, role_history |
personal |
location, location_history, education, interests |
knowledge |
expertise, top_domains, skills |
goals |
active, completed |
preferences |
a list of values |
candidates |
speculative, intended or ended values. Never current state. |
Each value has value, confidence (0 to 1) and source_ids (the memories behind it). A field with no value is None, or [] for lists. A user whose profile is not ready yet gets an empty card. Keys are snake_case.
get_profile() is deprecated and will be removed in 0.4.0. It returns a broad memory search, not the Profile Card.
Live context
Live context lets Vity see work the user is doing right now in other apps, such as ChatGPT or Claude. When the current message continues one of those chats, the context comes from that chat.
# With recall: also checks live chats.
context = client.recall("Continue the plan", live_context=True, session_key="conversation-8f2c")
# On its own: only the live chat, or "" if the prompt continues none.
live = client.live_context("Continue the plan", session_key="conversation-8f2c")
live_context(current_prompt, session_key="") is cheap. It does no memory retrieval when nothing matches. Use it on the first turn of a session if you do your own retrieval.
Pass the same session_key and agent_id that you use in capture(). Vity uses them to tell this session's own chat apart from the user's other chats.
Meetings
| Method | Returns |
|---|---|
meetings_enabled() |
bool. False when meeting memory is off or the call fails. |
meeting_ask(question, max_memories=12, meeting_type="", status="", seed_person_id="") |
dict |
get_meeting(external_id) |
dict |
list_meeting_reviews() |
list[dict] |
meeting_ask() answers a question from meeting memory. It is metered per call, so call it on demand. question is up to 4,000 characters. max_memories is 1 to 50. meeting_type (for example "1:1" or "standup") and status (for example "open") filter the meetings. seed_person_id boosts meetings with that person.
Preview methods
These methods are not yet generally available: store_many, get, update, delete, list and stats. When the service does not offer one, you get a VityNotFoundError whose message ends with "this is a preview feature that is not available yet". Do not depend on them yet.
client.store_many(items)
client.get(memory_id)
client.update(memory_id, content=None, category=None, importance=None)
client.delete(memory_id)
client.list(limit=50, offset=0, category="", since=None)
client.stats()
Async usage
AsyncVityClient takes the same arguments and has the same methods, as coroutines. Pass an httpx.AsyncClient as http_client if you need your own.
import asyncio
from maximem_vity import AsyncVityClient
async def main() -> None:
async with AsyncVityClient(channel="support-bot") as client:
context = await client.recall("What does the user like to eat?")
await client.capture(
[{"role": "user", "content": "I like Thai food."}],
session_key="conversation-8f2c",
)
asyncio.run(main())
Close the client with async with, or await client.close() (also available as aclose()).
Use as an agent tool
as_tool() lets a model search and save memories itself. It gives you two tools, search_memory and save_memory, in OpenAI or Anthropic format.
from openai import OpenAI
from maximem_vity import VityClient
llm, vity = OpenAI(), VityClient(channel="assistant")
memory = vity.as_tool(style="openai") # or style="anthropic"
messages = [{"role": "user", "content": "What am I allergic to?"}]
response = llm.chat.completions.create(model="gpt-4o", messages=messages, tools=memory.definitions)
message = response.choices[0].message
messages.append(message)
for call in message.tool_calls or []:
if memory.handles(call.function.name):
result = memory.call(call.function.name, call.function.arguments)
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
as_tool(style="openai", read_only=False).styleis"openai"or"anthropic".definitionsis plain JSON. Pass it astools.call(name, arguments)takes arguments as a JSON string or a dict, and returns a JSON string.search_memorytakesqueryand an optionallimit(default 5).save_memorytakescontentand optionalcategoryandimportance.- If the model sends invalid arguments,
call()returns{"error": ...}so the model can try again. Other problems, such as an invalid API key, raise an exception. An unknown tool name raisesValueError. read_only=Trueoffers onlysearch_memory.handles(name)tells you whether a tool call is for these tools.- The model chooses the query or the fact. It cannot change whose memory is used.
With AsyncVityClient, use await memory.call(...).
Errors
Every exception inherits from VityError.
| Exception | HTTP status | Retryable | Raised when |
|---|---|---|---|
VityInvalidInputError |
400, 422 | No | An argument is invalid. Usually raised before any request is sent. |
VityAuthError |
401, 403 | No | The API key is missing, invalid or revoked. |
VityInsufficientCreditsError |
402 | No | The account has no credits left. |
VityNotFoundError |
404 | No | The item does not exist. For a preview method, the feature is not available. |
VityRateLimitError |
429 | Yes | Too many requests. Check retry_after. |
VityUnavailableError |
5xx except 501, or no response | Yes | The service is temporarily unavailable, the network failed, or the request timed out. |
VityError |
any other | No | Any other failure, including a success response that is not JSON. |
Fields on every exception:
| Field | Meaning |
|---|---|
message |
A description of the problem. Also str(err). |
status_code |
The HTTP status, or None for errors raised on your side or by the network. |
code |
A machine-readable code from the service, if any. |
request_id |
The id the SDK sent with the request. Include it when you contact support. |
retry_after |
Seconds the service asked you to wait, if any. |
retryable |
True if trying again later may work. |
import time
from maximem_vity import VityError, VityInsufficientCreditsError, VityRateLimitError
try:
client.store("User is vegetarian", memory_type="preference")
except VityInsufficientCreditsError:
... # ask the account owner to add credits
except VityRateLimitError as err:
time.sleep(err.retry_after or 60)
except VityError as err:
print(f"Vity request failed: {err} (request id: {err.request_id})")
Invalid client settings raise ValueError: a timeout that is not above 0, a negative max_retries, or an endpoint that does not start with http:// or https://.
Retries and timeouts
The client retries temporary failures (HTTP 429, 5xx, network errors and timeouts) up to max_retries times.
- The wait starts at about 0.5 seconds, doubles each time, includes random jitter and never exceeds 8 seconds.
- If the service sends a
retry_afterof more than 8 seconds, the client does not wait. It raisesVityRateLimitErrorwithretry_afterset so you can decide what to do. capture,store,store_manyandforget(dry_run=False)are not retried after the request may have reached the server. They are retried only when the connection failed before sending, or when the service answered 429, which means it did no work.- All other calls, including a
forgetdry run, are retried.
timeout applies to each stage of a request (connect, send, receive). Connecting never waits longer than 10 seconds.
Logs go to the maximem_vity logger: retries at WARNING, each request at DEBUG. The API key is never logged.
Testing your code
Pass an httpx.MockTransport to test without network access:
import httpx
from maximem_vity import VityClient
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"context": "User prefers dark mode", "sources": []})
http = httpx.Client(transport=httpx.MockTransport(handler))
client = VityClient(api_key="mx_...", http_client=http)
assert client.recall("theme?") == "User prefers dark mode"
Python and JS differences
Both packages share one API. The names follow each language's style.
Python (maximem-vity-sdk) |
JavaScript (@maximem/vity-sdk) |
|
|---|---|---|
| Method names | snake_case: get_profile_card |
camelCase: getProfileCard |
| Arguments | Keyword arguments: search(q, top_k=5) |
Options object: search(q, { topK: 5 }) |
| Async | AsyncVityClient |
Every method returns a promise |
| Cancellation | Cancel the asyncio task | signal option on every method (AbortSignal) |
| Error fields | status_code, request_id, retry_after |
statusCode, requestId, retryAfter, cause |
| Timeout | Applies to each stage of a request | Applies to the whole attempt |
| Your own HTTP layer | http_client |
fetch |
| Search dates | created_at, chat_at, happened_at |
createdAt, chatAt, happenedAt |
| Knowledge filter keys | snake_case or camelCase | camelCase |
| Closing | close() or with |
close() does nothing |
| Unknown tool name | ValueError |
RangeError |
| Logging | maximem_vity logger |
None |
Requirements
- Python 3.9 or later.
httpx0.24 or later, below 1.0. Pip installs it for you.- The package ships type information (
py.typed).
Versions and links
This is version 0.3.2. The Python and JS packages share one version number. Version 0.3.2 changed only the documentation. What changed in 0.3.1:
- Added
get_profile_card(). - Search results now include
chat_atandhappened_at, so dates say when a memory was said and not only when it was stored. - Message
timestampis converted to epoch milliseconds. Before, a value in seconds was read as a date in 1970. get_profile()is deprecated.
Coming from 0.2.x:
user_idis ignored and will be removed in 0.4.0.channelandagent_iddefault to"unspecified". Setchannel.forget(dry_run=False)with no query needsdelete_all=True.search(min_score=0.0)applies no minimum, so you may see more results.- Invalid arguments raise
VityInvalidInputErrorbefore any request is sent.
Links: documentation and API keys.
License
MIT.
Metadata
Release files for maximem-vity-sdk 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| maximem_vity_sdk-0.3.2.tar.gz | 50.4 kB | Without provenance | Details for maximem_vity_sdk-0.3.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| maximem_vity_sdk-0.3.2-py3-none-any.whl | Python 3 | none | any | Details for maximem_vity_sdk-0.3.2-py3-none-any.whl |
Total release size: 85.4 kB
Release files / maximem_vity_sdk-0.3.2.tar.gz
| Download URL | maximem_vity_sdk-0.3.2.tar.gz |
|---|---|
| Size | 50.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
56e7afe7b3fa156d5cde02cf3f21d3e41b631e9f5bd59b82cc18b219420fede0
|
|
BLAKE2b-256 checksum How to use checksums |
d344e4a287ab804bf47f10896fc1389e4ca14878505d5560e26363619e6d24ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|
Release files / maximem_vity_sdk-0.3.2-py3-none-any.whl
| Download URL | maximem_vity_sdk-0.3.2-py3-none-any.whl |
|---|---|
| Size | 35.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e33168b195a54418f09964014c7b5a0427c88491b14b02e2b3562ae42740080a
|
|
BLAKE2b-256 checksum How to use checksums |
14bbbcbcc208a5e053d62656de028aa79ac540a3820170edc18a03f8185fadba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.9
|
Security for maximem-vity-sdk 0.3.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.