Skip to content

Commit 94914c0

Browse files
committed
get leaf id as input
1 parent 7e3c6bc commit 94914c0

File tree

10 files changed

+291
-48
lines changed

10 files changed

+291
-48
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
@@ -227,6 +227,7 @@ async def store_message(
227227
*,
228228
blob: MessageBlob,
229229
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
230+
parent_id: str | None = None,
230231
meta: dict[str, Any] | None = None,
231232
file_field: str | None = None,
232233
file: (
@@ -242,6 +243,7 @@ async def store_message(
242243
session_id: The UUID of the session.
243244
blob: The message blob in Acontext, OpenAI, Anthropic, or Gemini format.
244245
format: The format of the message blob. Defaults to "openai".
246+
parent_id: Optional parent message UUID for branching. Defaults to None.
245247
meta: Optional user-provided metadata for the message. This metadata is stored
246248
separately from the message content and can be retrieved via get_messages().metas
247249
or updated via patch_message_meta(). Works with all formats.
@@ -271,6 +273,8 @@ async def store_message(
271273
payload: dict[str, Any] = {
272274
"format": format,
273275
}
276+
if parent_id is not None:
277+
payload["parent_id"] = parent_id
274278
if meta is not None:
275279
payload["meta"] = meta
276280

@@ -368,6 +372,7 @@ async def get_messages(
368372
*,
369373
limit: int | None = None,
370374
cursor: str | None = None,
375+
leaf_id: str | None = None,
371376
with_asset_public_url: bool | None = None,
372377
with_events: bool | None = None,
373378
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
@@ -381,6 +386,7 @@ async def get_messages(
381386
session_id: The UUID of the session.
382387
limit: Maximum number of messages to return. Defaults to None.
383388
cursor: Cursor for pagination. Defaults to None.
389+
leaf_id: Optional leaf message UUID to read one root-to-leaf branch path. Defaults to None.
384390
with_asset_public_url: Whether to include presigned URLs for assets. Defaults to None.
385391
format: The format of the messages. Defaults to "openai". Supports "acontext", "openai", "anthropic", or "gemini".
386392
time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
@@ -403,9 +409,19 @@ async def get_messages(
403409
Returns:
404410
GetMessagesOutput containing the list of messages and pagination information.
405411
"""
412+
if leaf_id is not None:
413+
if limit is not None:
414+
raise ValueError("leaf_id cannot be combined with limit")
415+
if cursor is not None:
416+
raise ValueError("leaf_id cannot be combined with cursor")
417+
if time_desc is not None:
418+
raise ValueError("leaf_id cannot be combined with time_desc")
419+
406420
params: dict[str, Any] = {}
407421
if format is not None:
408422
params["format"] = format
423+
if leaf_id is not None:
424+
params["leaf_id"] = leaf_id
409425
params.update(
410426
build_params(
411427
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
@@ -227,6 +227,7 @@ def store_message(
227227
*,
228228
blob: MessageBlob,
229229
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
230+
parent_id: str | None = None,
230231
meta: dict[str, Any] | None = None,
231232
file_field: str | None = None,
232233
file: (
@@ -242,6 +243,7 @@ def store_message(
242243
session_id: The UUID of the session.
243244
blob: The message blob in Acontext, OpenAI, Anthropic, or Gemini format.
244245
format: The format of the message blob. Defaults to "openai".
246+
parent_id: Optional parent message UUID for branching. Defaults to None.
245247
meta: Optional user-provided metadata for the message. This metadata is stored
246248
separately from the message content and can be retrieved via get_messages().metas
247249
or updated via patch_message_meta(). Works with all formats.
@@ -272,6 +274,8 @@ 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

@@ -368,6 +372,7 @@ def get_messages(
368372
*,
369373
limit: int | None = None,
370374
cursor: str | None = None,
375+
leaf_id: str | None = None,
371376
with_asset_public_url: bool | None = None,
372377
with_events: bool | None = None,
373378
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
@@ -381,6 +386,7 @@ def get_messages(
381386
session_id: The UUID of the session.
382387
limit: Maximum number of messages to return. Defaults to None.
383388
cursor: Cursor for pagination. Defaults to None.
389+
leaf_id: Optional leaf message UUID to read one root-to-leaf branch path. Defaults to None.
384390
with_asset_public_url: Whether to include presigned URLs for assets. Defaults to None.
385391
format: The format of the messages. Defaults to "openai". Supports "acontext", "openai", "anthropic", or "gemini".
386392
time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
@@ -403,9 +409,19 @@ def get_messages(
403409
Returns:
404410
GetMessagesOutput containing the list of messages and pagination information.
405411
"""
412+
if leaf_id is not None:
413+
if limit is not None:
414+
raise ValueError("leaf_id cannot be combined with limit")
415+
if cursor is not None:
416+
raise ValueError("leaf_id cannot be combined with cursor")
417+
if time_desc is not None:
418+
raise ValueError("leaf_id cannot be combined with time_desc")
419+
406420
params: dict[str, Any] = {}
407421
if format is not None:
408422
params["format"] = format
423+
if leaf_id is not None:
424+
params["leaf_id"] = leaf_id
409425
params.update(
410426
build_params(
411427
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: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export class SessionsAPI {
225225
blob: MessageBlob,
226226
options?: {
227227
format?: 'acontext' | 'openai' | 'anthropic' | 'gemini';
228+
parentId?: string | null;
228229
meta?: Record<string, unknown> | null;
229230
fileField?: string | null;
230231
file?: FileUpload | null;
@@ -243,6 +244,10 @@ export class SessionsAPI {
243244
format,
244245
};
245246

247+
if (options?.parentId !== undefined && options?.parentId !== null) {
248+
payload.parent_id = options.parentId;
249+
}
250+
246251
if (options?.meta !== undefined && options?.meta !== null) {
247252
payload.meta = options.meta;
248253
}
@@ -331,6 +336,7 @@ export class SessionsAPI {
331336
* @param options - Options for retrieving messages.
332337
* @param options.limit - Maximum number of messages to return.
333338
* @param options.cursor - Cursor for pagination.
339+
* @param options.leafId - Return only the root-to-leaf path for this leaf message ID.
334340
* @param options.withAssetPublicUrl - Whether to include presigned URLs for assets.
335341
* @param options.withEvents - Whether to include session events in the response.
336342
* @param options.format - The format of the messages ('acontext', 'openai', 'anthropic', or 'gemini').
@@ -355,6 +361,7 @@ export class SessionsAPI {
355361
options?: {
356362
limit?: number | null;
357363
cursor?: string | null;
364+
leafId?: string | null;
358365
withAssetPublicUrl?: boolean | null;
359366
withEvents?: boolean | null;
360367
format?: 'acontext' | 'openai' | 'anthropic' | 'gemini';
@@ -363,17 +370,32 @@ export class SessionsAPI {
363370
pinEditingStrategiesAtMessage?: string | null;
364371
}
365372
): Promise<GetMessagesOutput> {
373+
if (options?.leafId !== undefined && options?.leafId !== null) {
374+
if (options?.limit !== undefined && options?.limit !== null) {
375+
throw new Error('leafId cannot be combined with limit');
376+
}
377+
if (options?.cursor !== undefined && options?.cursor !== null) {
378+
throw new Error('leafId cannot be combined with cursor');
379+
}
380+
if (options?.timeDesc !== undefined && options?.timeDesc !== null) {
381+
throw new Error('leafId cannot be combined with timeDesc');
382+
}
383+
}
384+
366385
const params: Record<string, string | number> = {};
367386
if (options?.format !== undefined) {
368387
params.format = options.format;
369388
}
389+
if (options?.leafId !== undefined && options?.leafId !== null) {
390+
params.leaf_id = options.leafId;
391+
}
370392
Object.assign(
371393
params,
372394
buildParams({
373395
limit: options?.limit ?? null,
374396
cursor: options?.cursor ?? null,
375397
with_asset_public_url: options?.withAssetPublicUrl ?? null,
376-
time_desc: options?.timeDesc ?? true, // Default to true
398+
time_desc: options?.timeDesc ?? null,
377399
})
378400
);
379401
if (options?.withEvents !== undefined && options?.withEvents !== null) {

0 commit comments

Comments
 (0)