Metadata-Version: 2.4
Name: wontopos
Version: 2.2.18
Summary: Wontopos — long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100× lower LLM bill.
Author: Wontopos
License: MIT
Project-URL: Homepage, https://wontopos.com
Project-URL: Documentation, https://wontopos.com/en/why
Keywords: memory,ai,agent,llm,vector,embedding,semantic,rag,context
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
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: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.32
Provides-Extra: async
Requires-Dist: httpx>=0.27; extra == "async"
Dynamic: license-file

# Wontopos — long-term memory for AI agents

```bash
pip install wontopos
```

```python
from wontopos import Client

mem = Client(api_key="wos-...")
mem.add("she prefers tea over coffee", user_id="alice")

# one call → short-term + long-term + context, ready for your LLM prompt
ctx = mem.recall("what does alice drink?", user_id="alice")
```

## Why

- **Pure semantic retrieval** — no keyword/BM25. Identical recall in every language (한국어 · 日本語 · 中文 · English).
- **No LLM in the loop** — `store` / `search` / `recall` never call a language model. You pay embeddings, not generation.
- **Bounded retrieval** — `recall()` returns a small, fixed-size slice (~1,200 tokens) regardless of how much you've stored. Your LLM bill stops growing with history.

## Methods

| Method | Purpose |
|---|---|
| `add(content, user_id, **metadata)` | Store one memory |
| `add_turn(user_msg, assistant_msg, user_id?)` | Store a conversation exchange |
| `add_bulk(content, user_id, category=, timestamp=)` | Backfill a long history |
| `update(old_memory_id, new_content, user_id?)` | Supersede an old fact |
| `search(query, user_id, limit=10, **opts)` | Semantic search |
| `recall(query, user_id)` | One-call context (short + long + surrounding) |
| `history(user_id)` | Recent turns (short-term) |
| `stats(user_id)` | Counts |
| `get(user_id, memory_id)` | Fetch one memory by id (original text, no vector) |
| `list_memories(user_id, limit=100, cursor=)` | Browse/export a store's raw memories, paged |
| `delete(user_id, memory_id)` | Delete one memory |
| `delete_all(user_id)` | GDPR erase (delete every memory for the user) |
| `add_speaker(speaker, user_id?)` | Register a person (explicit, up to 50 to start) |
| `list_speakers(user_id?)` | Registered people + per-person memory counts |
| `remove_speaker(speaker, user_id?)` | Unregister; memories stay, the tag goes |

All methods take a `user_id` — it names the **store**: one isolated memory space per end-user, agent, or topic, then per account (your API key). WHO said each memory inside a store is the `speaker` tag below — storing the assistant's own words never needs a separate id.

## Who said it (speakers)

Every memory can carry a speaker: `"me"` for the assistant's own words, or a
person's name. Speakers are explicit, like stores: register a person once,
then store under their name — a typo can never silently become a new person.
Search accepts a speaker too, so you can recall one person's words only.

```python
mem.add_speaker("Bob", user_id="alice")      # once per person
mem.add("I promised to send the report on Friday", user_id="alice", speaker="me")
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.search("what did Bob say about deadlines?", user_id="alice", speaker="Bob")
```

A store registers up to 50 people to start (a limit we plan to raise);
`"me"` never needs registration and never counts against it.

## Async

Same surface, awaitable — needs the extra:

```bash
pip install "wontopos[async]"
```

```python
from wontopos import AsyncClient

async with AsyncClient(api_key="wos-...", user_id="alice") as mem:
    await mem.add("she prefers tea over coffee")
    hits = await mem.search("what does alice drink?")
```

Every `Client` method exists on `AsyncClient` with identical arguments and
semantics (retries, redirect refusal, guards). Close with `async with` or
`await mem.aclose()`.

## Recall caching

Opt in per search and repeated or extended queries reuse the previous result
at 10% of the normal rate (Tablet and Scroll models):

```python
hits = mem.search("...the conversation so far...", user_id="alice",
                  cache_control={"ttl": "5m"})   # or "1h"
```

## Reliability

Built in, no configuration needed:

- **Automatic retries** — 429 / 502 / 503 and connection errors retry twice with
  exponential backoff + jitter, honoring the server's `Retry-After`. Tune with
  `Client(retries=...)`; `retries=0` disables.
- **Redirects refused** — the API key never follows a 3xx to another host.
- **Timeouts** — 30s per attempt by default (`Client(timeout=...)`).
- **Key never in logs** — `repr(client)` masks the API key.
- **Wipe guard** — `delete()` without a `memory_id` raises instead of silently
  meaning "delete everything"; wiping a store is only ever the explicit
  `delete_all(user_id)` / `delete_store(user_id)`.

## Security

Built in, none of it configurable off:

- **TLS 1.2 floor** and certificate verification that cannot be disabled.
- **Redirects refused** — a 3xx is an error, so the key never follows one to
  another host.
- **Response size cap** — anything over 64MB is refused instead of buffered.
- **Key hygiene** — keys are trimmed (a stray newline from a file otherwise
  becomes a mystery 401) and inner whitespace is rejected; model names are
  validated before they reach a header.
- **`Client.from_env()`** reads `WONTOPOS_API_KEY` (or `WOS_API_KEY`) — keep
  keys out of source code.
- Plain-HTTP base URLs on non-local hosts warn. One dependency (`requests`,
  floor `>=2.32` for its certificate-verification fix).

## Errors

Any non-2xx response raises `WosError(status, message)`. When the server sent a
request id it's on `e.request_id` — include it when contacting support.

```python
from wontopos import Client, WosError

try:
    mem.search("...", user_id="alice")
except WosError as e:
    if e.status == 401:
        print("API key invalid or revoked")
    elif e.status == 429:
        print("Rate limited — back off")   # already retried twice by then
    else:
        print(e.status, e.message, e.request_id)
```

## Self-host

Point at your own engine:

```python
mem = Client(api_key="...", base_url="https://wos.your-host.com")
```

## Links

- Homepage: <https://wontopos.com>
- API reference: <https://wontopos.com/en/why> (Developers tab)

## Changelog

- **2.2.10** — docs: the memory relevance field is `similarity` (not `score`);
  responses are plain dicts, so no code change — version bumped for lockstep with
  the TS/Rust `Memory` type fix.
- **2.2.9** — `AsyncClient` (same surface, `pip install "wontopos[async]"`);
  security round 2: TLS 1.2 floor, 64MB response cap, key/model-name hygiene,
  `Client.from_env()`, `requests>=2.32` floor.
- **2.2.8** — reliability + hardening: automatic retries (429/502/503 + connection
  errors, backoff + jitter, honors `Retry-After`), redirects refused, key masked in
  `repr`, `WosError.request_id`, guard against `delete()` without `memory_id`,
  plain-HTTP warning, `User-Agent`, `with_model()`.
- **2.2.7** — speakers: `add_speaker` / `list_speakers` / `remove_speaker`.
- **2.2.6** — docs: `user_id` = store, clarified everywhere.
- **2.2.5** — docs: speakers + recall caching sections.
- **2.2.4** — Python/TypeScript/Rust converge on one version; all three now
  release in lockstep (same version, same surface). Patch releases are additive.

License: MIT.
