Metadata-Version: 2.4
Name: maximem-vity-sdk
Version: 0.3.0
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

Long-term memory for AI applications, by [Maximem AI](https://maximem.ai).

Store facts and preferences, capture conversations, and recall the right context for every prompt,
with a small, typed client for both synchronous and `asyncio` code.

```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? Install [`@maximem/vity-sdk`](https://www.npmjs.com/package/@maximem/vity-sdk) from npm.

## Contents

- [Installation](#installation)
- [Quick start](#quick-start)
- [Concepts](#concepts)
- [Configuration](#configuration)
- [API reference](#api-reference)
- [LLM tool calling](#llm-tool-calling)
- [Error handling](#error-handling)
- [Retries and timeouts](#retries-and-timeouts)
- [Testing](#testing)
- [Upgrading from 0.2.x](#upgrading-from-02x)
- [License](#license)

## Installation

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

Requires Python 3.9 or later. The package is fully typed.

## Quick start

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

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

A typical agent turn recalls memory before answering and captures the exchange afterwards:

```python
from maximem_vity import VityClient

with VityClient(channel="support-bot") as client:
    # Before answering: fetch relevant memory for the system prompt.
    context = client.recall("Can you book my usual table?")

    # After answering: capture 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 an explicit fact.
    client.store("Prefers window seats", memory_type="preference", importance="high")

    # Search memory directly.
    for hit in client.search("seating preferences", top_k=5):
        print(f"{hit['score']:.2f}  {hit['content']}")
```

### Async

`AsyncVityClient` takes the same arguments and exposes the same methods as coroutines:

```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?")

asyncio.run(main())
```

## Concepts

**Memory belongs to your account.** All API keys on a Maximem account share the same memory.
Rotating or adding keys never loses anything.

**Channels and sessions.** Set `channel` to the name of your integration (`"slack"`, `"support-bot"`).
Pass a `session_key` to `capture()` that stays the same for every turn of one conversation and
changes for each new conversation.

**`store()` or `capture()`?**

| | `store()` | `capture()` |
|---|---|---|
| Input | One explicit fact | A conversation (up to 50 messages) |
| Saved | Immediately | In the background, usually within seconds to minutes |
| What is kept | Exactly what you send | Durable facts only; small talk is ignored |

A successful `capture()` means the conversation was accepted for processing.

**`recall()` or `search()`?** `recall()` returns a text block ready to drop into a prompt.
`search()` returns scored memory records for your own logic.

## Configuration

| Argument | Environment variable | Default | Description |
|---|---|---|---|
| `api_key` | `MAXIMEM_API_KEY` | required | Your Maximem API key. |
| `endpoint` | `MAXIMEM_ENDPOINT` | Maximem API | Only needed for a non-default environment. |
| `channel` | | `"unspecified"` | The name of your integration. Recommended. |
| `agent_id` | | `"unspecified"` | Tag recorded on stored memories. |
| `timeout` | | `30.0` | Request timeout in seconds. |
| `max_retries` | | `2` | Retries for temporary failures. `0` disables them. |
| `http_client` | | managed | Your own `httpx.Client` / `httpx.AsyncClient` (proxies, custom TLS, tests). |

Arguments take precedence over environment variables.

## API reference

Every method is available on `VityClient` and, as a coroutine, on `AsyncVityClient`.

### Memory

| Method | Returns |
|---|---|
| `recall(current_prompt, recent_messages=None, max_tokens=1000, strategy="hybrid")` | `str`: prompt-ready context, or `""` |
| `capture(messages, session_key="", agent_id="", channel=None)` | `CaptureResult` |
| `store(content, memory_type="fact", category="", importance="medium")` | `StoreResult`: `{"id", "stored"}` |
| `search(query, top_k=10, category="", min_score=0.0)` | `list[SearchResult]`: `content`, `type`, `score`, `id`, `created_at` |
| `forget(query="", dry_run=True, delete_all=False)` | `ForgetResult`: `{"count", "ids"}` |
| `capture_status(request_id)` | `dict` |

- `strategy`: `"semantic"`, `"recency"` or `"hybrid"`.
- `memory_type`: `fact`, `preference`, `emotion`, `episode`, `knowledge` or `profile`.
  Use `category` (`preference`, `fact`, `task`, `relationship`, `context`) to set the category directly.
- `importance`: `"low"`, `"medium"` or `"high"`.

#### Deleting memories

`forget()` only previews unless you pass `dry_run=False`. Deleting without a query removes
**all** memories on the account, so it also requires `delete_all=True`:

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

### Knowledge

| Method | Returns |
|---|---|
| `knowledge(query="", limit=10, offset=0, min_score=0.3, filters=None, include=None, ...)` | `KnowledgeResult` |
| `knowledge_diff(input_text, purpose, ...)` | `KnowledgeDiffResult` |

`knowledge()` returns consolidated knowledge about the user (topics, summaries, confidence), with
pagination through `limit` and `offset`. Filter with keys such as `topic_category`,
`confidence_min`, `seen_after` and `seen_before`; choose sections with `include`
(`"items"`, `"facets"`, `"profile"`).

`knowledge_diff()` returns the knowledge most relevant to a piece of text. `purpose` describes what
you are looking for and why:

```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",
)
```

### Meetings

| Method | Returns |
|---|---|
| `meetings_enabled()` | `bool`: `False` if unavailable |
| `meeting_ask(question, max_memories=12, meeting_type="", status="", seed_person_id="")` | `dict` |
| `get_meeting(external_id)` | `dict` |
| `list_meeting_reviews()` | `list[dict]` |

## LLM tool calling

`as_tool()` lets a model search and save memories itself. It provides 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)

for call in response.choices[0].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})
```

- `definitions` is plain JSON and can be passed directly as `tools`.
- `call()` accepts arguments as a JSON string or a dict, and returns a JSON string.
- If the model sends invalid arguments, `call()` returns `{"error": ...}` so the model can retry.
  Other problems, such as an invalid API key, raise an exception.
- Pass `read_only=True` to offer only `search_memory`.
- The model chooses the query or the fact to save. It cannot change whose memory is used.

With `AsyncVityClient`, use `await memory.call(...)`.

## Error handling

All exceptions inherit from `VityError`:

| Exception | Raised when |
|---|---|
| `VityInvalidInputError` | An argument is invalid. Usually raised before any request is sent. |
| `VityAuthError` | The API key is missing, invalid or revoked. |
| `VityInsufficientCreditsError` | The account has run out of credits. |
| `VityNotFoundError` | The requested item does not exist. |
| `VityRateLimitError` | Too many requests. Check `retry_after`. |
| `VityUnavailableError` | A temporary service or network problem, or a timeout. |

Each exception has `message`, `status_code`, `code`, `request_id`, `retry_after` and `retryable`.
Include `request_id` when contacting support.

```python
import time
from maximem_vity import VityError, VityInsufficientCreditsError, VityRateLimitError

try:
    client.store("User is vegetarian", memory_type="preference")
except VityInsufficientCreditsError:
    ...  # prompt 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})")
```

## Retries and timeouts

The client retries temporary failures automatically, up to `max_retries` times with exponential
backoff:

- Read operations (`recall`, `search`, `knowledge`, ...) are retried after temporary errors.
- Write operations (`store`, `capture`, `forget`) are retried only when the request never reached
  the service. This prevents a memory from being saved twice.
- The client never waits more than a few seconds before retrying. If the service asks for a longer
  wait, you get a `VityRateLimitError` with `retry_after` set and can decide what to do.

Input is validated before sending. For example, `capture()` accepts up to 50 messages per call and
`store()` up to 10,000 characters, so mistakes fail fast with a clear message.

Logs go to the `maximem_vity` logger: retries at `WARNING`, requests at `DEBUG`. The API key is
never logged.

## Testing

Use an `httpx.MockTransport` to test your code 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": []})

client = VityClient(api_key="mx_test", http_client=httpx.Client(transport=httpx.MockTransport(handler)))
assert client.recall("theme?") == "User prefers dark mode"
```

## Upgrading from 0.2.x

0.3.0 is backwards compatible for typical use. Check these changes:

- **`user_id` is deprecated.** It is ignored because memory belongs to the account that owns the
  key. It will be removed in 0.4.0.
- **`channel` and `agent_id` default to `"unspecified"`.** Set `channel` to your integration's name.
- **Deleting everything requires confirmation:** `forget(dry_run=False, delete_all=True)`.
- **`search(min_score=0.0)` now means no minimum score**, so you may see more results.
- **Invalid arguments raise `VityInvalidInputError` before any request is sent.**
- **New exceptions** `VityInvalidInputError`, `VityInsufficientCreditsError` and `VityNotFoundError`
  inherit from `VityError`, so existing `except VityError` blocks keep working.


## License

MIT
