Skip to content

Commit ba8f60a

Browse files
committed
feat(api, sdk): finalization of the history deletion feature in the sdk
Signed-off-by: Jan Jeliga <jeliga.jan@gmail.com>
1 parent 7b598fc commit ba8f60a

File tree

7 files changed

+86
-15
lines changed

7 files changed

+86
-15
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
from collections.abc import AsyncIterator
6+
from uuid import UUID
67

78
import janus
89
from a2a.types import Artifact, Message, MessageSendConfiguration, Task
@@ -36,6 +37,11 @@ async def load_history(self) -> AsyncIterator[Message | Artifact]:
3637
async for item in self._store.load_history():
3738
yield item
3839

40+
async def delete_history_from_id(self, from_id: UUID) -> None:
41+
if not self._store:
42+
raise RuntimeError("Context store is not initialized")
43+
await self._store.delete_history_from_id(from_id)
44+
3945
def yield_sync(self, value: RunYield) -> RunYieldResume:
4046
self._yield_queue.sync_q.put(value)
4147
return self._yield_resume_queue.sync_q.get()

apps/agentstack-sdk-py/src/agentstack_sdk/server/store/context_store.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import abc
77
from collections.abc import AsyncIterator
88
from typing import TYPE_CHECKING, Protocol
9+
from uuid import UUID
910

1011
from a2a.types import Artifact, Message
1112

@@ -19,6 +20,8 @@ async def load_history(self) -> AsyncIterator[Message | Artifact]:
1920

2021
async def store(self, data: Message | Artifact) -> None: ...
2122

23+
async def delete_history_from_id(self, from_id: UUID) -> None: ...
24+
2225

2326
class ContextStore(abc.ABC):
2427
def modify_dependencies(self, dependencies: dict[str, Depends]) -> None:

apps/agentstack-sdk-py/src/agentstack_sdk/server/store/memory_context_store.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from collections.abc import AsyncIterator
55
from datetime import timedelta
6+
from uuid import UUID
67

78
from a2a.types import Artifact, Message
89
from cachetools import TTLCache
@@ -23,6 +24,15 @@ async def load_history(self) -> AsyncIterator[Message | Artifact]:
2324
async def store(self, data: Message | Artifact) -> None:
2425
self._history.append(data.model_copy(deep=True))
2526

27+
async def delete_history_from_id(self, from_id: UUID) -> None:
28+
# Does not allow to delete from an artifact onwards
29+
index = next(
30+
(i for i, item in enumerate(self._history) if isinstance(item, Message) and item.message_id == from_id),
31+
None,
32+
)
33+
if index is not None:
34+
self._history = self._history[:index]
35+
2636

2737
class InMemoryContextStore(ContextStore):
2838
def __init__(self, max_contexts: int = 1000, context_ttl: timedelta = timedelta(hours=1)):

apps/agentstack-sdk-py/src/agentstack_sdk/server/store/platform_context_store.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
from collections.abc import AsyncIterator
5+
from uuid import UUID
56

67
from a2a.types import Artifact, Message
78

@@ -46,3 +47,7 @@ async def load_history(self) -> AsyncIterator[Message | Artifact]:
4647
async def store(self, data: Message | Artifact) -> None:
4748
async with self._platform_extension.use_client():
4849
await Context.add_history_item(self._context_id, data=data)
50+
51+
async def delete_history_from_id(self, from_id: UUID) -> None:
52+
async with self._platform_extension.use_client():
53+
await Context.delete_history_from_id(self._context_id, from_id=from_id)

apps/agentstack-sdk-py/tests/e2e/test_history.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ async def get_final_task_from_stream(stream: AsyncIterator[ClientEvent | Message
2828
return final_task
2929

3030

31+
async def send_message_get_response(
32+
client: Client, content: str, context_id: str | None = None
33+
) -> tuple[list[str], str]:
34+
message = create_text_message_object(content=content)
35+
if context_id is not None:
36+
message.context_id = context_id
37+
final_task = await get_final_task_from_stream(client.send_message(message))
38+
agent_messages = [msg.parts[0].root.text for msg in final_task.history or []]
39+
return agent_messages, final_task.context_id
40+
41+
3142
@pytest.fixture
3243
async def history_agent(create_server_with_agent) -> AsyncGenerator[tuple[Server, Client]]:
3344
"""Agent that tests context.store.load_history() functionality."""
@@ -44,25 +55,40 @@ async def history_agent(input: Message, context: RunContext) -> AsyncGenerator[R
4455
yield server, client
4556

4657

58+
@pytest.fixture
59+
async def history_deleting_agent(create_server_with_agent) -> AsyncGenerator[tuple[Server, Client]]:
60+
"""Agent that tests context.store.load_history() functionality."""
61+
context_store = InMemoryContextStore()
62+
63+
async def history_agent(input: Message, context: RunContext) -> AsyncGenerator[RunYield, None]:
64+
await context.store(input)
65+
n_messages = 0
66+
async for message in context.load_history():
67+
n_messages += 1
68+
if n_messages == 1:
69+
delete_id = message.message_id
70+
if n_messages > 3:
71+
await context.delete_history_from_id(delete_id)
72+
73+
async for message in context.load_history():
74+
message.role = Role.agent
75+
yield message
76+
77+
async with create_server_with_agent(history_agent, context_store=context_store) as (server, client):
78+
yield server, client
79+
80+
4781
async def test_agent_history(history_agent):
4882
"""Test that history starts empty."""
4983
_, client = history_agent
50-
message = create_text_message_object(content="first message")
5184

52-
final_task = await get_final_task_from_stream(client.send_message(message))
53-
agent_messages = [msg.parts[0].root.text for msg in final_task.history]
85+
agent_messages, context_id = await send_message_get_response(client, "first message")
5486
assert agent_messages == ["first message"]
5587

56-
message = create_text_message_object(content="second message")
57-
message.context_id = final_task.context_id
58-
final_task = await get_final_task_from_stream(client.send_message(message))
59-
agent_messages = [msg.parts[0].root.text for msg in final_task.history]
88+
agent_messages, context_id = await send_message_get_response(client, "second message", context_id=context_id)
6089
assert agent_messages == ["first message", "first message", "second message"]
6190

62-
message = create_text_message_object(content="third message")
63-
message.context_id = final_task.context_id
64-
final_task = await get_final_task_from_stream(client.send_message(message))
65-
agent_messages = [msg.parts[0].root.text for msg in final_task.history]
91+
agent_messages, context_id = await send_message_get_response(client, "third message", context_id=context_id)
6692
assert agent_messages == [
6793
# first run
6894
"first message",
@@ -75,3 +101,23 @@ async def test_agent_history(history_agent):
75101
"second message",
76102
"third message",
77103
]
104+
105+
106+
async def test_agent_deleting_history(history_deleting_agent):
107+
"""Test that history starts empty."""
108+
_, client = history_deleting_agent
109+
110+
agent_messages, context_id = await send_message_get_response(client, "first message")
111+
assert agent_messages == ["first message"]
112+
113+
agent_messages, context_id = await send_message_get_response(client, "second message", context_id=context_id)
114+
assert agent_messages == ["first message", "second message"]
115+
116+
agent_messages, context_id = await send_message_get_response(client, "third message", context_id=context_id)
117+
assert agent_messages == ["first message", "second message", "third message"]
118+
119+
agent_messages, context_id = await send_message_get_response(client, "delete message", context_id=context_id)
120+
assert agent_messages == []
121+
122+
agent_messages, context_id = await send_message_get_response(client, "first message")
123+
assert agent_messages == ["first message"]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ async def list_history(
214214
)
215215

216216
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"""
217+
"""Delete all history items from a specific item onwards (inclusive) in given context"""
218218
# First, get the created_at timestamp of the item to delete from
219219
query_item = select(context_history_table.c.created_at).where(
220220
context_history_table.c.id == from_id,

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,15 @@ async def edit_message_in_context(run_context: RunContext, message_id: str, new_
300300
# Step 1: Delete from this message onwards
301301
await context.delete_history_from_id(from_id=message_id)
302302

303-
# Step 2: Add the corrected message
303+
# Step 2: Create the corrected message
304304
corrected_message = Message(
305305
message_id=str(uuid.uuid4()),
306-
parts=[Part(root=TextPart(text=new_text))],
306+
parts=[Part(TextPart(text=new_text))],
307307
role=Role.user,
308-
context_id=context.id,
309308
metadata=metadata,
310309
)
310+
311+
# Step 3: Store the corrected message
311312
await run_context.store(data=corrected_message)
312313
```
313314

0 commit comments

Comments
 (0)