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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Add a conversation; EverOS turns it into structured, retrievable memory you can
gets sharper the more your agent is used.
- **Structured memory, not chat logs** — extracts episodes, user profiles, and reusable
agent cases & skills from raw conversations, so retrieval returns meaning, not transcripts.
- **Retrieval that fits the query** — keyword, vector, hybrid (default), or agentic multi-step search.
- **Retrieval that fits the query** — keyword, vector, hybrid (default), or agentic multi-step search,
plus Context Retrieval for questions that need several rounds of searching.
- **Knowledge bases** — ingest documents into a searchable topic library alongside
conversational memory; ingest is async and reports progress through the task API.
- **Multimodal** — attach images, audio, and documents to any message.
Expand Down Expand Up @@ -94,7 +95,7 @@ multimodal upload — is in

`EverOS` covers the common calls with plain kwargs in and the response's `.data` out.
Memory methods are bare verbs named after their route (`add`, `search`, `get`, `flush`,
`edit`, `delete`, `update`, `delete_by_ids`, `feedback`); every other resource is
`edit`, `delete`, `update`, `delete_by_ids`, `feedback`, `context`); every other resource is
`<resource>_<verb>` (`kb_create`, `doc_ingest`, `task_wait`, `tag_bind`), so typing
`client.kb` lists the knowledge-base surface.

Expand Down
37 changes: 35 additions & 2 deletions everos_cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
``upload`` (storage), ``close``. They are public API and cannot be renamed. Memory
methods added since follow the same bare-verb shape so the memory group stays one
style and each name is the last segment of its route — ``update`` / ``delete_by_ids``
/ ``feedback`` (1.2.0). Every other resource carries its name as a prefix — ``kb_`` /
/ ``feedback`` (1.2.0), ``context`` (1.3.0). Every other resource carries its name as a prefix — ``kb_`` /
``doc_`` / ``task_`` / ``tag_`` — so completion groups by resource and no facade method
collides with the generated method it wraps.

Expand All @@ -40,6 +40,7 @@
AddOperation,
Content,
ContentItem,
ContextInput,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: include the generated context endpoint and model before this facade lands.

ContextInput is absent from the package, and MemoryApi does not define retrieve_context. Importing everos_cloud.client therefore raises ImportError; because the top-level __init__.py swallows that error, from everos_cloud import EverOS fails too, breaking the entire existing facade rather than only context(). The clean-environment test run stops during collection on this import, and both CI versions reproduce it. Please add or regenerate the 1.3.0 model, API, exports, and contract artifacts alongside this wrapper.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Fixed by rebasing onto the merged 1.3.0-rc1 generated SDK: ContextInput is exported, MemoryApi.retrieve_context targets /api/v2/memory/context, and EverOS imports normally again. A fresh environment now reports 239 passed, and the Python 3.12, Python 3.14, and build checks all pass. I am closing this thread.

DeleteByIdsInput,
DeleteInput,
DeleteOperation,
Expand Down Expand Up @@ -87,7 +88,7 @@
"add_memory": "add", "search_memory": "search", "get_memory": "get",
"flush_memory": "flush", "edit_profile": "edit", "delete_memory": "delete",
"update_memory": "update", "delete_memories_by_ids": "delete_by_ids",
"submit_feedback": "feedback",
"submit_feedback": "feedback", "retrieve_context": "context",
"sign_objects": "presign",
"bind_tags": "tag_bind", "unbind_tags": "tag_unbind", "replace_tags": "tag_replace",
"create_knowledge_base": "kb_create", "list_knowledge_bases": "kb_list",
Expand Down Expand Up @@ -364,6 +365,38 @@ def search(
)
return self._call(self.memory.search_memory, payload).data

def context(
self,
query: str,
*,
user_id: str,
top_k: int | None = None,
include_profile: bool | None = None,
filters: Any = None,
app_id: str | None = None,
project_id: str | None = None,
) -> Any:
"""Context Retrieval: an LLM searches in rounds until the evidence suffices.

Returns ``SearchData``, the same shape as :meth:`search` — memories, not an
answer. ``episodes[].cited`` marks the ones the model's answer rested on, and
``score`` is a rank-fusion score, not comparable with search scores. User owners
only. The server caps the loop at 45 s and returns what it found so far, which
fits the client's default 60 s timeout; a client built with a shorter timeout
should raise it for this call's sake.
"""
payload = ContextInput(
**_clean(
query=query,
user_id=user_id,
top_k=top_k,
include_profile=include_profile,
filters=filters,
**self._scope(app_id, project_id),
)
)
return self._call(self.memory.retrieve_context, payload).data

def get(
self,
memory_type: str,
Expand Down
11 changes: 10 additions & 1 deletion quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ print(page.episodes)
result = client.search("outdoor hobbies", user_id="user-1", top_k=10, include_profile=True)
print(result.episodes)

# ── Context Retrieval (1.3.0) ─────────────────────────────────────────────────
# An LLM searches in rounds until it can answer the question, then returns the
# memories it found (not an answer). Seconds, not milliseconds: keep it off the hot
# path. Same result shape as search; cited marks what the answer rested on.
result = client.context("What did I decide about the Q3 budget after the finance meeting?",
user_id="user-1", top_k=10)
cited = [ep for ep in result.episodes if ep.cited] or result.episodes[:5]

# ── Edit a user's profile (bulk) ──────────────────────────────────────────────
# action: add | update | delete · type: explicit_info | implicit_traits
client.edit("user-1", operations=[
Expand Down Expand Up @@ -157,7 +165,7 @@ context manager (`with EverOS(...) as client:`) to release connections on exit.

Memory methods are bare verbs named after the last segment of their route (`add` /
`search` / `get` / `flush` / `edit` / `delete` since 1.0.0, `update` / `delete_by_ids` /
`feedback` since 1.2.0), as are `presign` / `upload` for storage and `close`. Every other
`feedback` since 1.2.0, `context` since 1.3.0), as are `presign` / `upload` for storage and `close`. Every other
resource is named `<resource>_<verb>`, so typing `client.kb` / `client.doc` /
`client.task` / `client.tag` lists everything for that resource.

Expand All @@ -167,6 +175,7 @@ resource is named `<resource>_<verb>`, so typing `client.kb` / `client.doc` /
| `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. |
| `context(query, user_id, ...)` | `POST /api/v2/memory/context` | Multi-round LLM retrieval for multi-hop / temporal questions; user owners only. |
| `edit(user_id, operations)` | `POST /api/v2/memory/edit` | Bulk profile add / update / delete. |
| `delete(...)` | `POST /api/v2/memory/delete` | Scoped soft-delete (whole user / agent / session). |
| `update(memory_id, ...)` | `POST /api/v2/memory/update` | Patch one episode's text / summary / subject; `unchanged` in the result when nothing differed. |
Expand Down
33 changes: 33 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from everos_cloud.exceptions import ApiException
from everos_cloud.models import (
AddInput,
ContextInput,
DeleteByIdsInput,
DocIngestBody,
DocIngestData,
Expand Down Expand Up @@ -82,6 +83,38 @@ def test_search_drops_none_so_defaults_survive():
assert payload.method == "hybrid" # model default preserved (not overwritten with None)


def test_context_builds_input_and_returns_data():
mem = MagicMock()
mem.retrieve_context.return_value = SimpleNamespace(data="C")
c = _client(memory=mem)

assert c.context("what changed after the finance meeting?", user_id="u1", top_k=5) == "C"
payload = mem.retrieve_context.call_args.args[0]
assert isinstance(payload, ContextInput)
assert payload.user_id == "u1" and payload.top_k == 5
assert (payload.app_id, payload.project_id) == ("default", "default")
assert mem.retrieve_context.call_args.kwargs["_request_timeout"] == 60.0


def test_context_sends_none_of_the_fields_the_server_rejects():
"""/context answers 422 to method != "context", min_score, radius,
enable_llm_rerank and with_readable_episode; the body must carry none of them."""
mem = MagicMock()
mem.retrieve_context.return_value = SimpleNamespace(data=None)
c = _client(memory=mem)

c.context("q", user_id="u1", include_profile=True)
body = mem.retrieve_context.call_args.args[0].to_dict()
assert body.get("method", "context") == "context"
for field in ("min_score", "radius", "enable_llm_rerank", "with_readable_episode"):
assert field not in body


def test_context_requires_a_user():
with pytest.raises(TypeError):
_client(memory=MagicMock()).context("q")


def test_get_and_delete():
mem = MagicMock()
mem.get_memory.return_value = SimpleNamespace(data="G")
Expand Down
Loading