Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand Down
30 changes: 11 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,36 +55,28 @@ 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

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=[
{"sender_id": "user-1", "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", user_id="user-1")
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

Expand Down
2 changes: 1 addition & 1 deletion everos_cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
235 changes: 79 additions & 156 deletions quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,195 +3,118 @@
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

```sh
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=[
{"sender_id": "user-1", "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. 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. 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) ──────────────────────────────────────────────
# 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=1700000000000,
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/).
Expand Down
6 changes: 3 additions & 3 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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():
Expand Down