From 0651b343dab966ef993290334b377ffaedad8750 Mon Sep 17 00:00:00 2001 From: George-iam Date: Sat, 28 Feb 2026 16:24:20 +0000 Subject: [PATCH] feat: add inbox list, thread, and reply client methods Implement owner-scoped inbox retrieval and thread reply helpers in the Python SDK with tests and quickstart updates aligned to Track C parity. Made-with: Cursor --- README.md | 9 ++++++ axme_sdk/client.py | 42 ++++++++++++++++++++++++++ tests/test_client.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/README.md b/README.md index b99701e..3d80198 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,15 @@ with AxmeClient(config) as client: idempotency_key="create-intent-001", ) print(result) + inbox = client.list_inbox(owner_agent="agent://example/receiver") + print(inbox) + replied = client.reply_inbox_thread( + "11111111-1111-4111-8111-111111111111", + message="Acknowledged", + owner_agent="agent://example/receiver", + idempotency_key="reply-001", + ) + print(replied) ``` ## Development diff --git a/axme_sdk/client.py b/axme_sdk/client.py index a5afcd3..e900029 100644 --- a/axme_sdk/client.py +++ b/axme_sdk/client.py @@ -65,3 +65,45 @@ def create_intent( if response.status_code >= 400: raise AxmeHttpError(response.status_code, response.text) return response.json() + + def list_inbox(self, *, owner_agent: str | None = None) -> dict[str, Any]: + params: dict[str, str] | None = None + if owner_agent is not None: + params = {"owner_agent": owner_agent} + response = self._http.get("/v1/inbox", params=params) + if response.status_code >= 400: + raise AxmeHttpError(response.status_code, response.text) + return response.json() + + def get_inbox_thread(self, thread_id: str, *, owner_agent: str | None = None) -> dict[str, Any]: + params: dict[str, str] | None = None + if owner_agent is not None: + params = {"owner_agent": owner_agent} + response = self._http.get(f"/v1/inbox/{thread_id}", params=params) + if response.status_code >= 400: + raise AxmeHttpError(response.status_code, response.text) + return response.json() + + def reply_inbox_thread( + self, + thread_id: str, + *, + message: str, + owner_agent: str | None = None, + idempotency_key: str | None = None, + ) -> dict[str, Any]: + params: dict[str, str] | None = None + if owner_agent is not None: + params = {"owner_agent": owner_agent} + headers: dict[str, str] | None = None + if idempotency_key is not None: + headers = {"Idempotency-Key": idempotency_key} + response = self._http.post( + f"/v1/inbox/{thread_id}/reply", + params=params, + json={"message": message}, + headers=headers, + ) + if response.status_code >= 400: + raise AxmeHttpError(response.status_code, response.text) + return response.json() diff --git a/tests/test_client.py b/tests/test_client.py index 63cdaba..3884508 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -26,6 +26,28 @@ def _client(handler, api_key: str = "token") -> AxmeClient: return AxmeClient(cfg, http_client=http_client) +def _thread_payload() -> dict[str, object]: + return { + "thread_id": "11111111-1111-4111-8111-111111111111", + "intent_id": "22222222-2222-4222-8222-222222222222", + "status": "active", + "owner_agent": "agent://owner", + "from_agent": "agent://from", + "to_agent": "agent://to", + "created_at": "2026-02-28T00:00:00Z", + "updated_at": "2026-02-28T00:00:01Z", + "timeline": [ + { + "event_id": "33333333-3333-4333-8333-333333333333", + "event_type": "message.sent", + "actor": "gateway", + "at": "2026-02-28T00:00:01Z", + "details": {"message": "hello"}, + } + ], + } + + def test_health_success() -> None: def handler(request: httpx.Request) -> httpx.Response: assert request.method == "GET" @@ -94,3 +116,51 @@ def test_create_intent_raises_for_mismatched_correlation_id() -> None: }, correlation_id="11111111-1111-1111-1111-111111111111", ) + + +def test_list_inbox_success() -> None: + thread = _thread_payload() + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "GET" + assert request.url.path == "/v1/inbox" + assert request.url.params.get("owner_agent") == "agent://owner" + return httpx.Response(200, json={"ok": True, "threads": [thread]}) + + client = _client(handler) + assert client.list_inbox(owner_agent="agent://owner") == {"ok": True, "threads": [thread]} + + +def test_get_inbox_thread_success() -> None: + thread = _thread_payload() + thread_id = str(thread["thread_id"]) + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "GET" + assert request.url.path == f"/v1/inbox/{thread_id}" + assert request.url.params.get("owner_agent") == "agent://owner" + return httpx.Response(200, json={"ok": True, "thread": thread}) + + client = _client(handler) + assert client.get_inbox_thread(thread_id, owner_agent="agent://owner") == {"ok": True, "thread": thread} + + +def test_reply_inbox_thread_success() -> None: + thread = _thread_payload() + thread_id = str(thread["thread_id"]) + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == f"/v1/inbox/{thread_id}/reply" + assert request.url.params.get("owner_agent") == "agent://owner" + assert request.headers["idempotency-key"] == "reply-1" + assert request.read() == b'{"message":"ack"}' + return httpx.Response(200, json={"ok": True, "thread": thread}) + + client = _client(handler) + assert client.reply_inbox_thread( + thread_id, + message="ack", + owner_agent="agent://owner", + idempotency_key="reply-1", + ) == {"ok": True, "thread": thread}