Skip to content

Commit 4acbfff

Browse files
committed
get leaf id as input
1 parent 93d0f5f commit 4acbfff

File tree

10 files changed

+290
-47
lines changed

10 files changed

+290
-47
lines changed

src/client/acontext-py/src/acontext/resources/async_sessions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ async def store_message(
228228
*,
229229
blob: MessageBlob,
230230
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
231+
parent_id: str | None = None,
231232
meta: dict[str, Any] | None = None,
232233
file_field: str | None = None,
233234
file: (
@@ -243,6 +244,7 @@ async def store_message(
243244
session_id: The UUID of the session.
244245
blob: The message blob in Acontext, OpenAI, Anthropic, or Gemini format.
245246
format: The format of the message blob. Defaults to "openai".
247+
parent_id: Optional parent message UUID for branching. Defaults to None.
246248
meta: Optional user-provided metadata for the message. This metadata is stored
247249
separately from the message content and can be retrieved via get_messages().metas
248250
or updated via patch_message_meta(). Works with all formats.
@@ -272,6 +274,8 @@ async def store_message(
272274
payload: dict[str, Any] = {
273275
"format": format,
274276
}
277+
if parent_id is not None:
278+
payload["parent_id"] = parent_id
275279
if meta is not None:
276280
payload["meta"] = meta
277281

@@ -369,6 +373,7 @@ async def get_messages(
369373
*,
370374
limit: int | None = None,
371375
cursor: str | None = None,
376+
leaf_id: str | None = None,
372377
with_asset_public_url: bool | None = None,
373378
with_events: bool | None = None,
374379
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
@@ -382,6 +387,7 @@ async def get_messages(
382387
session_id: The UUID of the session.
383388
limit: Maximum number of messages to return. Defaults to None.
384389
cursor: Cursor for pagination. Defaults to None.
390+
leaf_id: Optional leaf message UUID to read one root-to-leaf branch path. Defaults to None.
385391
with_asset_public_url: Whether to include presigned URLs for assets. Defaults to None.
386392
format: The format of the messages. Defaults to "openai". Supports "acontext", "openai", "anthropic", or "gemini".
387393
time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
@@ -404,9 +410,19 @@ async def get_messages(
404410
Returns:
405411
GetMessagesOutput containing the list of messages and pagination information.
406412
"""
413+
if leaf_id is not None:
414+
if limit is not None:
415+
raise ValueError("leaf_id cannot be combined with limit")
416+
if cursor is not None:
417+
raise ValueError("leaf_id cannot be combined with cursor")
418+
if time_desc is not None:
419+
raise ValueError("leaf_id cannot be combined with time_desc")
420+
407421
params: dict[str, Any] = {}
408422
if format is not None:
409423
params["format"] = format
424+
if leaf_id is not None:
425+
params["leaf_id"] = leaf_id
410426
params.update(
411427
build_params(
412428
limit=limit,

src/client/acontext-py/src/acontext/resources/sessions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ def store_message(
228228
*,
229229
blob: MessageBlob,
230230
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
231+
parent_id: str | None = None,
231232
meta: dict[str, Any] | None = None,
232233
file_field: str | None = None,
233234
file: (
@@ -243,6 +244,7 @@ def store_message(
243244
session_id: The UUID of the session.
244245
blob: The message blob in Acontext, OpenAI, Anthropic, or Gemini format.
245246
format: The format of the message blob. Defaults to "openai".
247+
parent_id: Optional parent message UUID for branching. Defaults to None.
246248
meta: Optional user-provided metadata for the message. This metadata is stored
247249
separately from the message content and can be retrieved via get_messages().metas
248250
or updated via patch_message_meta(). Works with all formats.
@@ -273,6 +275,8 @@ def store_message(
273275
payload: dict[str, Any] = {
274276
"format": format,
275277
}
278+
if parent_id is not None:
279+
payload["parent_id"] = parent_id
276280
if meta is not None:
277281
payload["meta"] = meta
278282

@@ -369,6 +373,7 @@ def get_messages(
369373
*,
370374
limit: int | None = None,
371375
cursor: str | None = None,
376+
leaf_id: str | None = None,
372377
with_asset_public_url: bool | None = None,
373378
with_events: bool | None = None,
374379
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
@@ -382,6 +387,7 @@ def get_messages(
382387
session_id: The UUID of the session.
383388
limit: Maximum number of messages to return. Defaults to None.
384389
cursor: Cursor for pagination. Defaults to None.
390+
leaf_id: Optional leaf message UUID to read one root-to-leaf branch path. Defaults to None.
385391
with_asset_public_url: Whether to include presigned URLs for assets. Defaults to None.
386392
format: The format of the messages. Defaults to "openai". Supports "acontext", "openai", "anthropic", or "gemini".
387393
time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
@@ -404,9 +410,19 @@ def get_messages(
404410
Returns:
405411
GetMessagesOutput containing the list of messages and pagination information.
406412
"""
413+
if leaf_id is not None:
414+
if limit is not None:
415+
raise ValueError("leaf_id cannot be combined with limit")
416+
if cursor is not None:
417+
raise ValueError("leaf_id cannot be combined with cursor")
418+
if time_desc is not None:
419+
raise ValueError("leaf_id cannot be combined with time_desc")
420+
407421
params: dict[str, Any] = {}
408422
if format is not None:
409423
params["format"] = format
424+
if leaf_id is not None:
425+
params["leaf_id"] = leaf_id
410426
params.update(
411427
build_params(
412428
limit=limit,

src/client/acontext-py/tests/test_async_client.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,38 @@ def read(self) -> bytes:
210210
assert attachment[2] == "image/png"
211211

212212

213+
@patch("acontext.async_client.AcontextAsyncClient.request", new_callable=AsyncMock)
214+
@pytest.mark.asyncio
215+
async def test_async_store_message_forwards_parent_id(
216+
mock_request, async_client: AcontextAsyncClient
217+
) -> None:
218+
mock_request.return_value = {
219+
"id": "msg-id",
220+
"session_id": "session-id",
221+
"parent_id": "parent-msg-id",
222+
"role": "user",
223+
"meta": {},
224+
"parts": [],
225+
"session_task_process_status": "pending",
226+
"created_at": "2024-01-01T00:00:00Z",
227+
"updated_at": "2024-01-01T00:00:00Z",
228+
}
229+
230+
await async_client.sessions.store_message(
231+
"session-id",
232+
blob={"role": "user", "content": "hi"}, # type: ignore[arg-type]
233+
format="openai",
234+
parent_id="parent-msg-id",
235+
)
236+
237+
mock_request.assert_called_once()
238+
args, kwargs = mock_request.call_args
239+
method, path = args
240+
assert method == "POST"
241+
assert path == "/session/session-id/messages"
242+
assert kwargs["json_data"]["parent_id"] == "parent-msg-id"
243+
244+
213245
@patch("acontext.async_client.AcontextAsyncClient.request", new_callable=AsyncMock)
214246
@pytest.mark.asyncio
215247
async def test_async_store_message_allows_nullable_blob_for_other_formats(
@@ -480,6 +512,45 @@ async def test_async_sessions_get_messages_forwards_format(
480512
assert hasattr(result, "has_more")
481513

482514

515+
@patch("acontext.async_client.AcontextAsyncClient.request", new_callable=AsyncMock)
516+
@pytest.mark.asyncio
517+
async def test_async_sessions_get_messages_forwards_leaf_id(
518+
mock_request, async_client: AcontextAsyncClient
519+
) -> None:
520+
mock_request.return_value = {
521+
"items": [],
522+
"ids": ["leaf-msg-id"],
523+
"has_more": False,
524+
"this_time_tokens": 0,
525+
}
526+
527+
result = await async_client.sessions.get_messages(
528+
"session-id", format="acontext", leaf_id="leaf-msg-id"
529+
)
530+
531+
mock_request.assert_called_once()
532+
args, kwargs = mock_request.call_args
533+
method, path = args
534+
assert method == "GET"
535+
assert path == "/session/session-id/messages"
536+
assert kwargs["params"] == {"format": "acontext", "leaf_id": "leaf-msg-id"}
537+
assert hasattr(result, "items")
538+
assert hasattr(result, "has_more")
539+
540+
541+
@patch("acontext.async_client.AcontextAsyncClient.request", new_callable=AsyncMock)
542+
@pytest.mark.asyncio
543+
async def test_async_sessions_get_messages_rejects_leaf_id_with_limit(
544+
mock_request, async_client: AcontextAsyncClient
545+
) -> None:
546+
with pytest.raises(ValueError, match="leaf_id cannot be combined with limit"):
547+
await async_client.sessions.get_messages(
548+
"session-id", leaf_id="leaf-msg-id", limit=10
549+
)
550+
551+
mock_request.assert_not_called()
552+
553+
483554
@patch("acontext.async_client.AcontextAsyncClient.request", new_callable=AsyncMock)
484555
@pytest.mark.asyncio
485556
async def test_async_sessions_get_messages_rejects_non_positive_gt_token(

src/client/acontext-py/tests/test_client.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,35 @@ def read(self) -> bytes:
143143
assert attachment[2] == "image/png"
144144

145145

146+
@patch("acontext.client.AcontextClient.request")
147+
def test_store_message_forwards_parent_id(mock_request, client: AcontextClient) -> None:
148+
mock_request.return_value = {
149+
"id": "msg-id",
150+
"session_id": "session-id",
151+
"parent_id": "parent-msg-id",
152+
"role": "user",
153+
"meta": {},
154+
"parts": [],
155+
"session_task_process_status": "pending",
156+
"created_at": "2024-01-01T00:00:00Z",
157+
"updated_at": "2024-01-01T00:00:00Z",
158+
}
159+
160+
client.sessions.store_message(
161+
"session-id",
162+
blob={"role": "user", "content": "hi"}, # type: ignore[arg-type]
163+
format="openai",
164+
parent_id="parent-msg-id",
165+
)
166+
167+
mock_request.assert_called_once()
168+
args, kwargs = mock_request.call_args
169+
method, path = args
170+
assert method == "POST"
171+
assert path == "/session/session-id/messages"
172+
assert kwargs["json_data"]["parent_id"] == "parent-msg-id"
173+
174+
146175
@patch("acontext.client.AcontextClient.request")
147176
def test_store_message_allows_nullable_blob_for_other_formats(
148177
mock_request, client: AcontextClient
@@ -553,6 +582,41 @@ def test_sessions_get_messages_forwards_format(
553582
assert hasattr(result, "has_more")
554583

555584

585+
@patch("acontext.client.AcontextClient.request")
586+
def test_sessions_get_messages_forwards_leaf_id(
587+
mock_request, client: AcontextClient
588+
) -> None:
589+
mock_request.return_value = {
590+
"items": [],
591+
"ids": ["leaf-msg-id"],
592+
"has_more": False,
593+
"this_time_tokens": 0,
594+
}
595+
596+
result = client.sessions.get_messages(
597+
"session-id", format="acontext", leaf_id="leaf-msg-id"
598+
)
599+
600+
mock_request.assert_called_once()
601+
args, kwargs = mock_request.call_args
602+
method, path = args
603+
assert method == "GET"
604+
assert path == "/session/session-id/messages"
605+
assert kwargs["params"] == {"format": "acontext", "leaf_id": "leaf-msg-id"}
606+
assert hasattr(result, "items")
607+
assert hasattr(result, "has_more")
608+
609+
610+
@patch("acontext.client.AcontextClient.request")
611+
def test_sessions_get_messages_rejects_leaf_id_with_limit(
612+
mock_request, client: AcontextClient
613+
) -> None:
614+
with pytest.raises(ValueError, match="leaf_id cannot be combined with limit"):
615+
client.sessions.get_messages("session-id", leaf_id="leaf-msg-id", limit=10)
616+
617+
mock_request.assert_not_called()
618+
619+
556620
@patch("acontext.client.AcontextClient.request")
557621
def test_sessions_get_messages_with_edit_strategies(
558622
mock_request, client: AcontextClient

src/client/acontext-ts/src/resources/sessions.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export class SessionsAPI {
229229
blob: MessageBlob,
230230
options?: {
231231
format?: 'acontext' | 'openai' | 'anthropic' | 'gemini';
232+
parentId?: string | null;
232233
meta?: Record<string, unknown> | null;
233234
fileField?: string | null;
234235
file?: FileUpload | null;
@@ -247,6 +248,10 @@ export class SessionsAPI {
247248
format,
248249
};
249250

251+
if (options?.parentId !== undefined && options?.parentId !== null) {
252+
payload.parent_id = options.parentId;
253+
}
254+
250255
if (options?.meta !== undefined && options?.meta !== null) {
251256
payload.meta = options.meta;
252257
}
@@ -335,6 +340,7 @@ export class SessionsAPI {
335340
* @param options - Options for retrieving messages.
336341
* @param options.limit - Maximum number of messages to return.
337342
* @param options.cursor - Cursor for pagination.
343+
* @param options.leafId - Return only the root-to-leaf path for this leaf message ID.
338344
* @param options.withAssetPublicUrl - Whether to include presigned URLs for assets.
339345
* @param options.withEvents - Whether to include session events in the response.
340346
* @param options.format - The format of the messages ('acontext', 'openai', 'anthropic', or 'gemini').
@@ -359,6 +365,7 @@ export class SessionsAPI {
359365
options?: {
360366
limit?: number | null;
361367
cursor?: string | null;
368+
leafId?: string | null;
362369
withAssetPublicUrl?: boolean | null;
363370
withEvents?: boolean | null;
364371
format?: 'acontext' | 'openai' | 'anthropic' | 'gemini';
@@ -367,10 +374,25 @@ export class SessionsAPI {
367374
pinEditingStrategiesAtMessage?: string | null;
368375
}
369376
): Promise<GetMessagesOutput> {
377+
if (options?.leafId !== undefined && options?.leafId !== null) {
378+
if (options?.limit !== undefined && options?.limit !== null) {
379+
throw new Error('leafId cannot be combined with limit');
380+
}
381+
if (options?.cursor !== undefined && options?.cursor !== null) {
382+
throw new Error('leafId cannot be combined with cursor');
383+
}
384+
if (options?.timeDesc !== undefined && options?.timeDesc !== null) {
385+
throw new Error('leafId cannot be combined with timeDesc');
386+
}
387+
}
388+
370389
const params: Record<string, string | number> = {};
371390
if (options?.format !== undefined) {
372391
params.format = options.format;
373392
}
393+
if (options?.leafId !== undefined && options?.leafId !== null) {
394+
params.leaf_id = options.leafId;
395+
}
374396
Object.assign(
375397
params,
376398
buildParams({

0 commit comments

Comments
 (0)