Skip to content

Commit 7b598fc

Browse files
committed
feat(api, sdk): adding 'delete from selected id onward' endpoint and sdk functionality
Signed-off-by: Jan Jeliga <jeliga.jan@gmail.com>
1 parent 20480d9 commit 7b598fc

File tree

7 files changed

+140
-0
lines changed

7 files changed

+140
-0
lines changed

apps/agentstack-sdk-py/src/agentstack_sdk/platform/context.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,21 @@ async def add_history_item(
238238
)
239239
).raise_for_status()
240240

241+
async def delete_history_from_id(
242+
self: Context | str,
243+
*,
244+
from_id: UUID | str,
245+
client: PlatformClient | None = None,
246+
) -> None:
247+
"""Delete all history items from a specific item onwards (inclusive)"""
248+
target_context_id = self if isinstance(self, str) else self.id
249+
async with client or get_platform_client() as platform_client:
250+
_ = (
251+
await platform_client.delete(
252+
url=f"/api/v1/contexts/{target_context_id}/history", params={"from_id": str(from_id)}
253+
)
254+
).raise_for_status()
255+
241256
async def list_history(
242257
self: Context | str,
243258
*,

apps/agentstack-server/src/agentstack_server/api/routes/contexts.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,13 @@ async def list_context_history(
151151
pagination: Annotated[PaginationQuery, Query()],
152152
) -> PaginatedResult[ContextHistoryItem]:
153153
return await context_service.list_history(context_id=context_id, user=user.user, pagination=pagination)
154+
155+
156+
@router.delete("/{context_id}/history", status_code=status.HTTP_204_NO_CONTENT)
157+
async def delete_context_history_from_id(
158+
context_id: UUID,
159+
from_id: Annotated[UUID, Query()],
160+
context_service: ContextServiceDependency,
161+
user: Annotated[AuthorizedUser, Depends(RequiresContextPermissionsPath(context_data={"read", "write"}))],
162+
) -> None:
163+
await context_service.delete_history_from_id(context_id=context_id, from_id=from_id, user=user.user)

apps/agentstack-server/src/agentstack_server/domain/repositories/context.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ async def list_history(
4747
order_by: str = "created_at",
4848
order="desc",
4949
) -> PaginatedResult[ContextHistoryItem]: ...
50+
51+
async def delete_history_from_id(self, *, context_id: UUID, from_id: UUID) -> int: ...

apps/agentstack-server/src/agentstack_server/infrastructure/persistence/repositories/context.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,28 @@ async def list_history(
213213
has_more=result.has_more,
214214
)
215215

216+
async def delete_history_from_id(self, *, context_id: UUID, from_id: UUID) -> int:
217+
"""Delete all history items from a specific item onwards (inclusive) for a context"""
218+
# First, get the created_at timestamp of the item to delete from
219+
query_item = select(context_history_table.c.created_at).where(
220+
context_history_table.c.id == from_id,
221+
context_history_table.c.context_id == context_id,
222+
)
223+
result = await self._connection.execute(query_item)
224+
row = result.first()
225+
if not row:
226+
raise EntityNotFoundError("context_history_item", from_id)
227+
228+
created_at = row[0]
229+
230+
# Delete all history items from the specified item onwards (created_at >= the target item's created_at)
231+
query = delete(context_history_table).where(
232+
context_history_table.c.context_id == context_id,
233+
context_history_table.c.created_at >= created_at,
234+
)
235+
result = await self._connection.execute(query)
236+
return result.rowcount
237+
216238
def _row_to_context_history_item(self, row: Row) -> ContextHistoryItem:
217239
return ContextHistoryItem(
218240
id=row.id,

apps/agentstack-server/src/agentstack_server/service_layer/services/contexts.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,12 @@ async def list_history(
271271
order=pagination.order,
272272
order_by=pagination.order_by,
273273
)
274+
275+
async def delete_history_from_id(self, *, context_id: UUID, from_id: UUID, user: User) -> None:
276+
"""Delete all history items from a specific item onwards (inclusive)"""
277+
async with self._uow() as uow:
278+
# Verify user has access to this context
279+
await uow.contexts.get(context_id=context_id, user_id=user.id)
280+
# Delete history items from the specified ID onwards
281+
await uow.contexts.delete_history_from_id(context_id=context_id, from_id=from_id)
282+
await uow.commit()

apps/agentstack-server/tests/e2e/routes/test_contexts.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,43 @@ async def test_context_provider_filtering(subtests):
269269
with subtests.test("get context includes provider_id"):
270270
fetched_context = await Context.get(context_with_provider1.id)
271271
assert fetched_context.provider_id == provider1.id
272+
273+
274+
@pytest.mark.usefixtures("clean_up", "setup_platform_client")
275+
async def test_delete_context_history_from_id(subtests):
276+
"""Test deleting context history from a specific item ID onwards."""
277+
278+
context = None
279+
history_items = []
280+
n_messages = 3
281+
282+
with subtests.test("create context and add multiple history items"):
283+
context = await Context.create()
284+
for i in range(n_messages):
285+
message = AgentMessage(text=f"Test message {i}")
286+
await context.add_history_item(data=message)
287+
288+
history = await context.list_history(limit=50)
289+
history_items = history.items
290+
assert len(history.items) == n_messages
291+
292+
with subtests.test("delete history from a middle item onwards"):
293+
await context.delete_history_from_id(from_id=history_items[1].id)
294+
295+
remaining_history = await context.list_history(limit=50)
296+
remaining_ids = [item.id for item in remaining_history.items]
297+
assert len(remaining_history.items) == 1
298+
assert history_items[0].id in remaining_ids
299+
assert history_items[1].id not in remaining_ids
300+
assert history_items[2].id not in remaining_ids
301+
302+
with subtests.test("delete with nonexistent item_id raises error"):
303+
nonexistent_id = uuid.uuid4()
304+
with pytest.raises(HTTPStatusError) as exc_info:
305+
await context.delete_history_from_id(from_id=nonexistent_id)
306+
assert exc_info.value.response.status_code == 404
307+
308+
with subtests.test("delete from first item deletes all"):
309+
await context.delete_history_from_id(from_id=remaining_ids[0])
310+
remaining_history = await context.list_history(limit=50)
311+
assert len(remaining_history.items) == 0

docs/development/agent-integration/multi-turn.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,45 @@ This advanced example demonstrates several key concepts:
276276
- **Memory Management**: Converts conversation history to framework format and loads it into agent memory
277277
- **Tool Usage**: Includes thinking tools and conditional requirements for better reasoning
278278
- **Persistent Storage**: Uses `PlatformContextStore` for conversation persistence
279+
280+
### Editing and Removing Messages from History
281+
282+
Sometimes you may need to edit a previous message in a conversation or remove messages that are no longer relevant.
283+
The Agent Stack provides a mechanism to delete history items from a specific point onward, allowing you to effectively “rewind” the conversation and replace a message with an edited version.
284+
Possible use cases include editing a previous message, clearing irrelevant exchanges, or removing messages that resulted from processing errors.
285+
286+
Here's an example of a function for editing a user message in a conversation using the context API, assuming you know the message id:
287+
288+
```python
289+
import uuid
290+
from typing import Any
291+
292+
from a2a.types import Message, Part, Role, TextPart
293+
from agentstack_sdk.platform.context import Context
294+
from agentstack_sdk.server.context import RunContext
295+
296+
async def edit_message_in_context(run_context: RunContext, message_id: str, new_text: str, metadata: dict[str, Any] | None = None):
297+
"""Edit a message in the conversation history"""
298+
context = await Context.get(run_context.context_id)
299+
300+
# Step 1: Delete from this message onwards
301+
await context.delete_history_from_id(from_id=message_id)
302+
303+
# Step 2: Add the corrected message
304+
corrected_message = Message(
305+
message_id=str(uuid.uuid4()),
306+
parts=[Part(root=TextPart(text=new_text))],
307+
role=Role.user,
308+
context_id=context.id,
309+
metadata=metadata,
310+
)
311+
await run_context.store(data=corrected_message)
312+
```
313+
314+
<Tip>
315+
When you delete history from a specific message onwards, all messages created after that point (including the message itself) are removed. This effectively creates a new conversation branch starting from the message before the deleted one.
316+
</Tip>
317+
318+
<Warning>
319+
This operation is permanent. Once messages are deleted, they cannot be recovered. Consider informing users about this operation or implementing a confirmation step for important conversations.
320+
</Warning>

0 commit comments

Comments
 (0)