Metadata-Version: 2.4
Name: maximem-vity-sdk
Version: 0.3.2
Summary: Vity by Maximem AI. Long-term memory for AI applications.
Author-email: Maximem AI <gaurav@maximem.ai>
License: MIT
Project-URL: Homepage, https://maximem.ai
Project-URL: Documentation, https://docs.maximem.ai/vity
Project-URL: Repository, https://github.com/maximem-ai/maximem-vity-sdk
Keywords: memory,ai,llm,agents,semantic-memory,vity,maximem
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1,>=0.24
Provides-Extra: dev
Requires-Dist: pytest<9,>=7; extra == "dev"
Requires-Dist: pytest-cov<7,>=4; extra == "dev"
Requires-Dist: build<2,>=1; extra == "dev"
Requires-Dist: twine<6,>=4; extra == "dev"
Dynamic: license-file

# 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](https://maximem.ai). It has a synchronous client and an `asyncio` client.

```python
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`](https://www.npmjs.com/package/@maximem/vity-sdk) on npm. It has the same methods. See [Python and JS differences](#python-and-js-differences).

## Contents

- [Install](#install)
- [Quick start](#quick-start)
- [Configuration](#configuration)
- [Core methods](#core-methods)
- [Async usage](#async-usage)
- [Use as an agent tool](#use-as-an-agent-tool)
- [Errors](#errors)
- [Retries and timeouts](#retries-and-timeouts)
- [Testing your code](#testing-your-code)
- [Python and JS differences](#python-and-js-differences)
- [Requirements](#requirements)
- [Versions and links](#versions-and-links)
- [License](#license)

## Install

```bash
pip install maximem-vity-sdk
```

## Quick start

Create an API key at [app.maximem.ai/api-keys](https://app.maximem.ai/api-keys) and set it in your environment:

```bash
export MAXIMEM_API_KEY="mx_..."
```

A typical agent turn recalls memory before it answers, then captures the conversation afterwards:

```python
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.
- **`channel` names your integration** (for example `"slack"` or `"support-bot"`). Together with `session_key` it identifies a conversation.
- **`session_key` identifies one conversation.** Use the same value for every turn of a conversation and a new value for each new conversation.

## Configuration

```python
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`](#recall) | Get memory as text to put in a prompt. |
| [`live_context`](#live-context) | Get only the user's live work from other apps. |
| [`capture`](#capture) | Send a conversation for background extraction. |
| [`store`](#store-and-search) | Save one fact now. |
| [`search`](#store-and-search) | Find memories by meaning, with scores. |
| [`forget`](#forget) | Preview or delete memories. |
| [`knowledge`, `knowledge_diff`](#knowledge) | Read consolidated knowledge about the user. |
| [`get_profile_card`](#profile-card) | Read the user's Profile Card. |
| [`meetings_enabled`, `meeting_ask`, `get_meeting`, `list_meeting_reviews`](#meetings) | Work with meeting memory. |
| [`store_many`, `get`, `update`, `delete`, `list`, `stats`](#preview-methods) | Preview. Not yet generally available. |

### Recall

```python
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_context` and `session_key`: see [Live context](#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

```python
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. `timestamp` is optional.
- `session_key`: the conversation's key. Use the same value on every turn of one conversation.
- `agent_id` and `channel`: 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`) are `None` for 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

```python
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`, `knowledge` or `profile`. This is a shortcut that maps to a category.
- `category`: overrides `memory_type`. One of `preference`, `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

```python
client.forget(query="", dry_run=True, delete_all=False) -> ForgetResult
```

`forget()` returns `{"count", "ids"}`. It only previews unless you pass `dry_run=False`.

```python
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

```python
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"`. Use `echo_slug` to choose the echo when the scope is `"echo"`.
- `limit` is 1 to 100 and `offset` is 0 to 10,000. Use them to page through results.
- `min_score` is 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 include `topic_subcategory`, `knowledge_type`, `expertise_level`, `seen_before` and `topic_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.

```python
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

```python
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.

```python
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.

```python
# 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.

```python
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.

```python
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.

```python
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)`. `style` is `"openai"` or `"anthropic"`.
- `definitions` is plain JSON. Pass it as `tools`.
- `call(name, arguments)` takes arguments as a JSON string or a dict, and returns a JSON string. `search_memory` takes `query` and an optional `limit` (default 5). `save_memory` takes `content` and optional `category` and `importance`.
- 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 raises `ValueError`.
- `read_only=True` offers only `search_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. |

```python
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_after` of more than 8 seconds, the client does not wait. It raises `VityRateLimitError` with `retry_after` set so you can decide what to do.
- `capture`, `store`, `store_many` and `forget(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 `forget` dry 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:

```python
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.
- `httpx` 0.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_at` and `happened_at`, so dates say when a memory was said and not only when it was stored.
- Message `timestamp` is 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_id` is ignored and will be removed in 0.4.0.
- `channel` and `agent_id` default to `"unspecified"`. Set `channel`.
- `forget(dry_run=False)` with no query needs `delete_all=True`.
- `search(min_score=0.0)` applies no minimum, so you may see more results.
- Invalid arguments raise `VityInvalidInputError` before any request is sent.

Links: [documentation](https://docs.maximem.ai/vity) and [API keys](https://app.maximem.ai/api-keys).

## License

MIT.
