From 7a4f3d5bf770d7ed529db9ddf941c338fd4ceda6 Mon Sep 17 00:00:00 2001 From: Dani Date: Fri, 31 Jul 2026 12:58:40 -0400 Subject: [PATCH 1/3] docs: lead the quickstart with the EverOS wrapper Now that the ergonomic EverOS client ships, show it as the primary usage instead of the low-level MemoryApi/ApiClient assembly. - README Quickstart: EverOS wrapper snippet (this is the PyPI project page). - quickstart.md: rewritten wrapper-first (add/flush/get/search/edit/delete/ upload via client.*), with the typed low-level client kept as an Advanced section (client.memory / client.storage). - ci.yml drift check: extended to map instance vars to their class, so it now validates EverOS wrapper methods in addition to MemoryApi/StorageApi. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 12 +- README.md | 28 ++--- quickstart.md | 235 +++++++++++++-------------------------- 3 files changed, 97 insertions(+), 178 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8ae973..792b966 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,8 +64,9 @@ jobs: python - <<'PY' import re import everos_cloud.models as models - from everos_cloud import MemoryApi, StorageApi + from everos_cloud import EverOS, MemoryApi, StorageApi + CLASSES = {"EverOS": EverOS, "MemoryApi": MemoryApi, "StorageApi": StorageApi} text = open("quickstart.md").read() blocks = re.findall(r"```python\n(.*?)```", text, re.S) assert blocks, "no python code blocks found in quickstart.md" @@ -83,9 +84,12 @@ jobs: if name: assert hasattr(models, name), f"quickstart drift: missing model {name!r}" - # 3) every API method the quickstart calls must still exist - for var, cls in (("memory", MemoryApi), ("storage", StorageApi)): - for meth in sorted(set(re.findall(rf"\b{var}\.(\w+)\(", code))): + # 3) every method called on an EverOS / MemoryApi / StorageApi instance + # must still exist (maps each instance var to its class from assignment). + var_class = {v: CLASSES[c] for v, c in + re.findall(r"(\w+)\s*=\s*(EverOS|MemoryApi|StorageApi)\(", code)} + for var, cls in var_class.items(): + for meth in sorted(set(re.findall(rf"\b{re.escape(var)}\.(\w+)\(", code))): assert hasattr(cls, meth), f"quickstart drift: {cls.__name__} has no {meth!r}" print("quickstart drift smoke OK") diff --git a/README.md b/README.md index 419ecd9..a1a6409 100644 --- a/README.md +++ b/README.md @@ -63,28 +63,20 @@ pip install everos-cloud Get an API key from the [EverOS Console](https://everos.evermind.ai), then: ```python -from everos_cloud import ApiClient, Configuration, MemoryApi -from everos_cloud.models import AddInput, MessageItem, Content, SearchInput +from everos_cloud import EverOS -config = Configuration(access_token="sk-...") +with EverOS(api_key="sk-...") as client: + client.add(session_id="session-1", messages=[ + {"role": "user", "content": "I love hiking in the mountains"}, + ]) -with ApiClient(config) as client: - memory = MemoryApi(client) - - memory.add_memory(AddInput( - session_id="session-1", - messages=[MessageItem( - sender_id="user-1", role="user", timestamp=1700000000, - content=Content("I love hiking in the mountains"), - )], - )) - - result = memory.search_memory(SearchInput(query="outdoor hobbies", method="hybrid")) - print(result.data) + results = client.search("outdoor hobbies") + print(results) ``` -**Full usage** — all six Memory endpoints, profile editing, deletion, and multimodal -uploads — is in **[quickstart.md](https://github.com/EverMind-AI/everos-cloud-sdk-python/blob/v1/quickstart.md)**. +**Full usage** — every memory operation, profile editing, and multimodal upload — is in +**[quickstart.md](https://github.com/EverMind-AI/everos-cloud-sdk-python/blob/v1/quickstart.md)**. +Prefer typed control? The generated low-level client is exposed as `client.memory` / `client.storage`. ## Documentation diff --git a/quickstart.md b/quickstart.md index 10c2dce..463d9ee 100644 --- a/quickstart.md +++ b/quickstart.md @@ -3,9 +3,8 @@ Full usage for the Python client of the **EverOS Cloud Memory API** (v2). For an overview and install, see the [README](README.md). -> **This code is generated** from the EverOS OpenAPI contract. Please file bugs and -> feature requests as issues — pull requests against the generated source will be -> overwritten on the next release. Corrections flow through the internal SDK factory. +> **This code is generated** from the EverOS OpenAPI contract, but the ergonomic +> `EverOS` client below is hand-maintained. File bugs and feature requests as issues. ## Install @@ -13,185 +12,109 @@ overview and install, see the [README](README.md). pip install everos-cloud ``` -Release candidates need `--pre`: `pip install --pre everos-cloud==1.0.0rc1`. +Release candidates need `--pre`: `pip install --pre everos-cloud`. -> **Upgrading from 0.4.x?** `everos-cloud` 1.0.0 is a **rewrite, not an increment**. The -> 0.x line was a different client (httpx-based, hand-maintained); 1.x is generated from -> the EverOS OpenAPI contract and has a different API surface — client classes, method -> names, and model types all changed. The import path (`everos_cloud`) is unchanged. -> Pin `everos-cloud<1` if you are not ready to migrate. - -## Authentication +## Quickstart -All requests use your EverOS API key as a bearer token: +`EverOS` is the recommended high-level client: plain kwargs / dicts in, the response +`.data` out. Get an API key from the [EverOS Console](https://everos.evermind.ai). ```python -from everos_cloud import Configuration -config = Configuration(access_token="sk-...") # sent as: Authorization: Bearer sk-... +from everos_cloud import EverOS + +client = EverOS(api_key="sk-...") # host defaults to https://api.evermind.ai + +# ── Add messages ────────────────────────────────────────────────────────────── +# Async by default: validated and enqueued, extraction runs in the background. +# `content` accepts a plain string; `timestamp` defaults to now, `sender_id` to role. +client.add(session_id="session-1", messages=[ + {"role": "user", "content": "I love hiking in the mountains"}, +]) + +# ── Force extraction for a session ──────────────────────────────────────────── +flushed = client.flush("session-1") +print(flushed.status) # "extracted" | "no_extraction" + +# ── Get memories (paginated) ────────────────────────────────────────────────── +# memory_type: episode | profile | agent_case | agent_skill +page = client.get("episode", page=1, page_size=20) +print(page.episodes) + +# ── Search ──────────────────────────────────────────────────────────────────── +# method: keyword | vector | hybrid (default) | agentic +result = client.search("outdoor hobbies", top_k=10, include_profile=True) +print(result.episodes) + +# ── Edit a user's profile (bulk) ────────────────────────────────────────────── +# action: add | update | delete · type: explicit_info | implicit_traits +client.edit("user-1", operations=[ + {"action": "add", "type": "explicit_info", + "data": {"category": "hobby", "description": "Enjoys hiking in the mountains"}, + "reason": "Stated in session-1"}, +]) + +# ── Delete memories (scoped soft-delete) ────────────────────────────────────── +client.delete(user_id="user-1", session_id="session-1") + +# ── Upload multimodal data ──────────────────────────────────────────────────── +# Presigns + POSTs the file directly to S3, returns the object key you then +# reference in a message's multimodal content. file_type is inferred from the ext. +object_key = client.upload("photo.jpg") ``` -The default host is `https://api.evermind.ai`; override with `Configuration(host=...)`. +Every method returns the endpoint's `.data`. Failures raise `EverOSError` +(`EverOSAPIError` for memory HTTP errors, `EverOSStorageError` for uploads). Set a +per-client request timeout with `EverOS(api_key=..., timeout=30)`, or use it as a +context manager (`with EverOS(...) as client:`) to release connections on exit. -## Quickstart +## Method reference + +| Method | Endpoint | Notes | +|---|---|---| +| `add(session_id, messages, ...)` | `POST /api/v2/memory/add` | Async by default (202 `queued`); `async_mode=False` for sync 200. | +| `flush(session_id)` | `POST /api/v2/memory/flush` | Force extraction for a session. | +| `get(memory_type, ...)` | `POST /api/v2/memory/get` | Paginated list by `memory_type`. | +| `search(query, ...)` | `POST /api/v2/memory/search` | Keyword / vector / hybrid / agentic. | +| `edit(user_id, operations)` | `POST /api/v2/memory/edit` | Bulk profile add / update / delete. | +| `delete(...)` | `POST /api/v2/memory/delete` | Scoped soft-delete. | +| `upload(path)` | `POST /api/v2/object/sign` + S3 | Presign + direct-to-S3, returns `object_key`. | + +## Low-level typed client (advanced) -The snippet below exercises the six Memory endpoints. Every Memory call takes a -single typed input model and returns a typed response envelope (`result.data`). -Object storage lives on a separate `StorageApi` (see below). +`EverOS` wraps the generated `MemoryApi` / `StorageApi`, exposed as `client.memory` +and `client.storage`. Use them directly when you want typed models and full control +— every request is a pydantic v2 model and every response a typed envelope +(`.data`). The full per-endpoint and model docs live under [`docs/`](docs/). ```python from everos_cloud import ApiClient, Configuration, MemoryApi -from everos_cloud.models import ( - AddInput, MessageItem, Content, SearchInput, GetInput, DeleteInput, - EditInput, AddOperation, EditInputOperationsInner, FlushInput, -) +from everos_cloud.models import AddInput, MessageItem, Content, SearchInput config = Configuration(access_token="sk-...") -with ApiClient(config) as client: - memory = MemoryApi(client) +with ApiClient(config) as api: + memory = MemoryApi(api) - # ── Add messages ────────────────────────────────────────────────────────── - # Async by default: returns HTTP 202 with status "queued" and extraction runs - # in the background. Pass async_mode=False to write synchronously and surface - # write errors directly. memory.add_memory(AddInput( session_id="session-1", - messages=[ - MessageItem( - sender_id="user-1", - role="user", # user | assistant | tool - timestamp=1700000000, - content=Content("I love hiking in the mountains"), - ) - ], + messages=[MessageItem( + sender_id="user-1", role="user", timestamp=1700000000, + content=Content("I love hiking in the mountains"), + )], )) - # ── Search memories ─────────────────────────────────────────────────────── - # method: keyword | vector | hybrid (default) | agentic - result = memory.search_memory(SearchInput( - query="outdoor hobbies", - method="hybrid", - top_k=10, - include_profile=True, - )) + result = memory.search_memory(SearchInput(query="outdoor hobbies", method="hybrid")) print(result.data) - - # ── Get memories (paginated) ────────────────────────────────────────────── - # memory_type: episode | profile | agent_case | agent_skill - page = memory.get_memory(GetInput( - memory_type="episode", - page=1, - page_size=20, - sort_order="desc", # by timestamp (default) - )) - print(page.data) - - # ── Edit profile (bulk, Cloud-only) ─────────────────────────────────────── - # 1–50 operations; each must be wrapped in EditInputOperationsInner. - # action: add | update | delete · type: explicit_info | implicit_traits - memory.edit_profile(EditInput( - user_id="user-1", - operations=[ - EditInputOperationsInner(AddOperation( - action="add", - type="explicit_info", - data={"category": "hobby", "description": "Enjoys hiking in the mountains"}, - reason="Stated in session-1", - )), - ], - )) - - # ── Delete memories (scoped soft-delete, Cloud-only) ────────────────────── - # Scope the delete by any combination of user_id / agent_id / session_id. - memory.delete_memory(DeleteInput(user_id="user-1", session_id="session-1")) - - # ── Flush a session (force extraction) ───────────────────────────────────── - # Extraction is normally async; flush forces it for a session and returns - # status "extracted" or "no_extraction". - flushed = memory.flush_memory(FlushInput(session_id="session-1")) - print(flushed.data.status) # "extracted" | "no_extraction" ``` -## Uploading multimodal data (`StorageApi`) - -Attaching images, audio, or documents to a message is a two-step flow: ask the -API to presign an upload, then `POST` the bytes directly to S3 using the returned -form fields. The sign endpoint lives on `StorageApi`. - -Unlike the Memory endpoints, `sign_objects` returns the raw MMS envelope: business -outcome is carried in `status` (`0` means success) and the payload in -`result.data` — check `status == 0` before reading it. +`MessageItem.content` accepts a plain string (shorthand for a single text item) or an +explicit list — both are passed through the `Content` wrapper: ```python -import requests -from everos_cloud import ApiClient, Configuration, StorageApi -from everos_cloud.models import SignRequest, SignObjectItem - -config = Configuration(access_token="sk-...") - -with ApiClient(config) as client: - storage = StorageApi(client) - - # ── Presign uploads (≤ 50 objects; each file_id unique) ──────────────────── - # file_type: image | file | video - envelope = storage.sign_objects(SignRequest( - object_list=[ - SignObjectItem(file_id="file-1", file_name="photo.jpg", file_type="image"), - ], - )) - - if envelope.status != 0: # 0 == success; see status codes below - raise RuntimeError(f"sign failed: status={envelope.status} error={envelope.error}") - - # ── Upload the bytes straight to S3 with the presigned POST form ─────────── - for obj in envelope.result.data.object_list: - signed = obj.object_signed_info # url + fields + maxSize - with open("photo.jpg", "rb") as fh: - resp = requests.post( - signed.url, - data=signed.fields, # presigned form fields - files={"file": fh}, - ) - resp.raise_for_status() # 204 from S3 on success - print(obj.object_key) # reference this key back in /add -``` - -Non-zero `status` values surface business errors rather than raising — common -ones are `2018` (validation failed), `1012` (bad/expired token), `1002` -(unsupported `file_type`), and `1007` (more than 50 objects). See the `signObjects` -description in `openapi.json` for the full list. - -## Methods - -`MemoryApi` mirrors the v2 endpoints: - -| Method | Endpoint | Notes | -|---|---|---| -| `add_memory(AddInput)` | `POST /api/v2/memory/add` | Async by default (202 `queued`); `async_mode=False` for sync 200. | -| `search_memory(SearchInput)` | `POST /api/v2/memory/search` | Keyword / vector / hybrid / agentic. | -| `get_memory(GetInput)` | `POST /api/v2/memory/get` | Paginated list by `memory_type`. | -| `delete_memory(DeleteInput)` | `POST /api/v2/memory/delete` | Scoped soft-delete. | -| `edit_profile(EditInput)` | `POST /api/v2/memory/edit` | Bulk profile add/update/delete operations. | -| `flush_memory(FlushInput)` | `POST /api/v2/memory/flush` | Force extraction for a session. | - -`StorageApi` covers object upload: - -| Method | Endpoint | Notes | -|---|---|---| -| `sign_objects(SignRequest)` | `POST /api/v1/object/sign` | Presign ≤ 50 objects for direct-to-S3 upload; returns the MMS envelope (`status == 0` on success). | - -### A note on message `content` - -`MessageItem.content` accepts either a plain string or a list of content items. In -this SDK both are passed through the `Content` wrapper: - -```python -Content("hello") # plain text (shorthand) -Content([ContentItem(type="text", text="hello")]) # explicit item list +Content("hello") # plain text +Content([ContentItem(type="text", text="hello")]) # explicit item list ``` -Either serializes to the correct wire shape. - ## Links - API reference: per-endpoint and model docs under [`docs/`](docs/). From 622a45c4a903ddefd9d0cc8467c0f2f0666b54b7 Mon Sep 17 00:00:00 2001 From: Dani Date: Fri, 31 Jul 2026 13:22:22 -0400 Subject: [PATCH 2/3] fix: timestamp default must be milliseconds; scope get/search examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live validation against the real v2 API surfaced two issues: - MessageItem.timestamp must be a unix MILLISECOND timestamp (>= 1e12); the wrapper defaulted to int(time.time()) (seconds), which the API rejects (422). Fixed to int(time.time() * 1000); tests + the low-level example use ms too. - get/search require user_id or agent_id — the quickstart/README examples omitted it (422). Added user_id (+ a sender_id) so the walkthrough is self-consistent. Verified: full live round-trip add->flush->get->search->upload->delete is green against api.evermind.ai; 18 unit tests + drift check green. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 ++-- everos_cloud/client.py | 2 +- quickstart.md | 12 ++++++------ tests/test_client.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a1a6409..8fb497c 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,10 @@ from everos_cloud import EverOS with EverOS(api_key="sk-...") as client: client.add(session_id="session-1", messages=[ - {"role": "user", "content": "I love hiking in the mountains"}, + {"sender_id": "user-1", "role": "user", "content": "I love hiking in the mountains"}, ]) - results = client.search("outdoor hobbies") + results = client.search("outdoor hobbies", user_id="user-1") print(results) ``` diff --git a/everos_cloud/client.py b/everos_cloud/client.py index 43597fb..cfb3ba1 100644 --- a/everos_cloud/client.py +++ b/everos_cloud/client.py @@ -158,7 +158,7 @@ def _to_message(m: MessageLike) -> MessageItem: sender_id=m.get("sender_id") or role, sender_name=m.get("sender_name"), role=role, - timestamp=m["timestamp"] if m.get("timestamp") is not None else int(time.time()), + timestamp=m["timestamp"] if m.get("timestamp") is not None else int(time.time() * 1000), content=content, tool_calls=m.get("tool_calls"), tool_call_id=m.get("tool_call_id"), diff --git a/quickstart.md b/quickstart.md index 463d9ee..a488cdb 100644 --- a/quickstart.md +++ b/quickstart.md @@ -28,7 +28,7 @@ client = EverOS(api_key="sk-...") # host defaults to https://api.evermind.ai # Async by default: validated and enqueued, extraction runs in the background. # `content` accepts a plain string; `timestamp` defaults to now, `sender_id` to role. client.add(session_id="session-1", messages=[ - {"role": "user", "content": "I love hiking in the mountains"}, + {"sender_id": "user-1", "role": "user", "content": "I love hiking in the mountains"}, ]) # ── Force extraction for a session ──────────────────────────────────────────── @@ -36,13 +36,13 @@ flushed = client.flush("session-1") print(flushed.status) # "extracted" | "no_extraction" # ── Get memories (paginated) ────────────────────────────────────────────────── -# memory_type: episode | profile | agent_case | agent_skill -page = client.get("episode", page=1, page_size=20) +# memory_type: episode | profile | agent_case | agent_skill. Scope with user_id or agent_id. +page = client.get("episode", user_id="user-1", page=1, page_size=20) print(page.episodes) # ── Search ──────────────────────────────────────────────────────────────────── -# method: keyword | vector | hybrid (default) | agentic -result = client.search("outdoor hobbies", top_k=10, include_profile=True) +# method: keyword | vector | hybrid (default) | agentic. Scope with user_id or agent_id. +result = client.search("outdoor hobbies", user_id="user-1", top_k=10, include_profile=True) print(result.episodes) # ── Edit a user's profile (bulk) ────────────────────────────────────────────── @@ -98,7 +98,7 @@ with ApiClient(config) as api: memory.add_memory(AddInput( session_id="session-1", messages=[MessageItem( - sender_id="user-1", role="user", timestamp=1700000000, + sender_id="user-1", role="user", timestamp=1700000000000, content=Content("I love hiking in the mountains"), )], )) diff --git a/tests/test_client.py b/tests/test_client.py index 8ceb376..74825e0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -33,7 +33,7 @@ def test_add_builds_payload_and_returns_data(): assert isinstance(msg, MessageItem) assert msg.role == "user" assert msg.sender_id == "user" # defaults to role when omitted - assert isinstance(msg.timestamp, int) # defaults to now + assert msg.timestamp >= 1_000_000_000_000 # defaults to now, in unix milliseconds # string content shorthand became a Content wrapper assert msg.content is not None @@ -43,10 +43,10 @@ def test_message_passthrough_and_explicit_fields(): mem.add_memory.return_value = SimpleNamespace(data=None) c = _client(memory=mem) c.add(session_id="s1", messages=[ - {"sender_id": "u9", "role": "assistant", "timestamp": 1700000000, "content": "hi"}, + {"sender_id": "u9", "role": "assistant", "timestamp": 1700000000000, "content": "hi"}, ]) msg = mem.add_memory.call_args.args[0].messages[0] - assert msg.sender_id == "u9" and msg.role == "assistant" and msg.timestamp == 1700000000 + assert msg.sender_id == "u9" and msg.role == "assistant" and msg.timestamp == 1700000000000 def test_search_drops_none_so_defaults_survive(): From 069a9a4295c2bc598f46781f576e9e42c65f215f Mon Sep 17 00:00:00 2001 From: Dani Date: Fri, 31 Jul 2026 13:35:31 -0400 Subject: [PATCH 3/3] docs: point README migration link at the 0.4.x->1.x guide Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fb497c..0dad926 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ pip install everos-cloud > Pre-releases need `--pre`: `pip install --pre everos-cloud`. > > Upgrading from the 0.4.x client? 1.x is a rewrite with a new API surface — see the -> [migration guide](https://docs.evermind.ai/api-reference/sdk-migration). Pin +> [migration guide](https://docs.evermind.ai/api-reference/sdk-migration-1x). Pin > `everos-cloud<1` to stay on the old client. ## Quickstart