feat: add conversation deletion endpoint (soft delete) [LET-7286] (#9230)
* feat: add conversation deletion endpoint (soft delete) [LET-7286]
- Add DELETE /conversations/{conversation_id} endpoint
- Filter soft-deleted conversations from list operations
- Add check_is_deleted=True to update/delete operations
Co-authored-by: Sarah Wooders <sarahwooders@users.noreply.github.com>
🤖 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
* feat: add tests, update SDK and stainless for delete conversation
- Add 5 integration tests for DELETE conversation endpoint
- Run stage-api to regenerate OpenAPI spec and SDK
- Add delete method to conversations in stainless.yml
Co-authored-by: Sarah Wooders <sarahwooders@users.noreply.github.com>
🤖 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
* test: add manager-level tests for conversation soft delete [LET-7286]
- test_delete_conversation_removes_from_list
- test_delete_conversation_double_delete_raises
- test_update_deleted_conversation_raises
- test_delete_conversation_excluded_from_summary_search
🤖 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
---------
Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com>
Co-authored-by: Letta <noreply@letta.com>
Co-authored-by: Sarah Wooders <sarahwooders@gmail.com>
This commit is contained in:
committed by
Caren Thomas
parent
9c8589a687
commit
ba67621e1b
@@ -567,6 +567,94 @@ class TestConversationsSDK:
|
||||
assert first_message_id not in [m.id for m in messages_after]
|
||||
|
||||
|
||||
class TestConversationDelete:
|
||||
"""Tests for the conversation delete endpoint."""
|
||||
|
||||
def test_delete_conversation(self, client: Letta, agent, server_url: str):
|
||||
"""Test soft deleting a conversation."""
|
||||
# Create a conversation
|
||||
conversation = client.conversations.create(agent_id=agent.id)
|
||||
assert conversation.id is not None
|
||||
|
||||
# Delete it via REST endpoint
|
||||
response = requests.delete(
|
||||
f"{server_url}/v1/conversations/{conversation.id}",
|
||||
)
|
||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}"
|
||||
|
||||
# Verify it's no longer accessible
|
||||
response = requests.get(
|
||||
f"{server_url}/v1/conversations/{conversation.id}",
|
||||
)
|
||||
assert response.status_code == 404, f"Expected 404 for deleted conversation, got {response.status_code}"
|
||||
|
||||
def test_delete_conversation_removes_from_list(self, client: Letta, agent, server_url: str):
|
||||
"""Test that deleted conversations don't appear in list."""
|
||||
# Create two conversations
|
||||
conv1 = client.conversations.create(agent_id=agent.id)
|
||||
conv2 = client.conversations.create(agent_id=agent.id)
|
||||
|
||||
# Verify both appear in list
|
||||
conversations = client.conversations.list(agent_id=agent.id)
|
||||
conv_ids = [c.id for c in conversations]
|
||||
assert conv1.id in conv_ids
|
||||
assert conv2.id in conv_ids
|
||||
|
||||
# Delete one
|
||||
response = requests.delete(
|
||||
f"{server_url}/v1/conversations/{conv1.id}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify only the non-deleted one appears in list
|
||||
conversations = client.conversations.list(agent_id=agent.id)
|
||||
conv_ids = [c.id for c in conversations]
|
||||
assert conv1.id not in conv_ids, "Deleted conversation should not appear in list"
|
||||
assert conv2.id in conv_ids, "Non-deleted conversation should still appear"
|
||||
|
||||
def test_delete_conversation_not_found(self, client: Letta, agent, server_url: str):
|
||||
"""Test that deleting a non-existent conversation returns 404."""
|
||||
fake_id = "conv-00000000-0000-0000-0000-000000000000"
|
||||
response = requests.delete(
|
||||
f"{server_url}/v1/conversations/{fake_id}",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_delete_conversation_double_delete(self, client: Letta, agent, server_url: str):
|
||||
"""Test that deleting an already-deleted conversation returns 404."""
|
||||
# Create and delete a conversation
|
||||
conversation = client.conversations.create(agent_id=agent.id)
|
||||
|
||||
# First delete should succeed
|
||||
response = requests.delete(
|
||||
f"{server_url}/v1/conversations/{conversation.id}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Second delete should return 404
|
||||
response = requests.delete(
|
||||
f"{server_url}/v1/conversations/{conversation.id}",
|
||||
)
|
||||
assert response.status_code == 404, "Double delete should return 404"
|
||||
|
||||
def test_update_deleted_conversation_fails(self, client: Letta, agent, server_url: str):
|
||||
"""Test that updating a deleted conversation returns 404."""
|
||||
# Create and delete a conversation
|
||||
conversation = client.conversations.create(agent_id=agent.id)
|
||||
|
||||
response = requests.delete(
|
||||
f"{server_url}/v1/conversations/{conversation.id}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Try to update the deleted conversation
|
||||
response = requests.patch(
|
||||
f"{server_url}/v1/conversations/{conversation.id}",
|
||||
json={"summary": "Updated summary"},
|
||||
)
|
||||
assert response.status_code == 404, "Updating deleted conversation should return 404"
|
||||
|
||||
|
||||
class TestConversationCompact:
|
||||
"""Tests for the conversation compact (summarization) endpoint."""
|
||||
|
||||
|
||||
@@ -166,6 +166,110 @@ async def test_delete_conversation(conversation_manager, server: SyncServer, sar
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_conversation_removes_from_list(conversation_manager, server: SyncServer, sarah_agent, default_user):
|
||||
"""Test that soft-deleted conversations are excluded from list results."""
|
||||
# Create two conversations
|
||||
conv1 = await conversation_manager.create_conversation(
|
||||
agent_id=sarah_agent.id,
|
||||
conversation_create=CreateConversation(summary="Keep me"),
|
||||
actor=default_user,
|
||||
)
|
||||
conv2 = await conversation_manager.create_conversation(
|
||||
agent_id=sarah_agent.id,
|
||||
conversation_create=CreateConversation(summary="Delete me"),
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
# Delete one
|
||||
await conversation_manager.delete_conversation(
|
||||
conversation_id=conv2.id,
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
# List should only return the non-deleted conversation
|
||||
conversations = await conversation_manager.list_conversations(
|
||||
agent_id=sarah_agent.id,
|
||||
actor=default_user,
|
||||
)
|
||||
conv_ids = [c.id for c in conversations]
|
||||
assert conv1.id in conv_ids
|
||||
assert conv2.id not in conv_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_conversation_double_delete_raises(conversation_manager, server: SyncServer, sarah_agent, default_user):
|
||||
"""Test that deleting an already-deleted conversation raises NoResultFound."""
|
||||
created = await conversation_manager.create_conversation(
|
||||
agent_id=sarah_agent.id,
|
||||
conversation_create=CreateConversation(summary="Delete me twice"),
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
await conversation_manager.delete_conversation(
|
||||
conversation_id=created.id,
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
# Second delete should raise
|
||||
with pytest.raises(NoResultFound):
|
||||
await conversation_manager.delete_conversation(
|
||||
conversation_id=created.id,
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_deleted_conversation_raises(conversation_manager, server: SyncServer, sarah_agent, default_user):
|
||||
"""Test that updating a soft-deleted conversation raises NoResultFound."""
|
||||
created = await conversation_manager.create_conversation(
|
||||
agent_id=sarah_agent.id,
|
||||
conversation_create=CreateConversation(summary="Original"),
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
await conversation_manager.delete_conversation(
|
||||
conversation_id=created.id,
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
with pytest.raises(NoResultFound):
|
||||
await conversation_manager.update_conversation(
|
||||
conversation_id=created.id,
|
||||
conversation_update=UpdateConversation(summary="Should fail"),
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_conversation_excluded_from_summary_search(conversation_manager, server: SyncServer, sarah_agent, default_user):
|
||||
"""Test that soft-deleted conversations are excluded from summary search results."""
|
||||
await conversation_manager.create_conversation(
|
||||
agent_id=sarah_agent.id,
|
||||
conversation_create=CreateConversation(summary="alpha search term"),
|
||||
actor=default_user,
|
||||
)
|
||||
to_delete = await conversation_manager.create_conversation(
|
||||
agent_id=sarah_agent.id,
|
||||
conversation_create=CreateConversation(summary="alpha deleted term"),
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
await conversation_manager.delete_conversation(
|
||||
conversation_id=to_delete.id,
|
||||
actor=default_user,
|
||||
)
|
||||
|
||||
results = await conversation_manager.list_conversations(
|
||||
agent_id=sarah_agent.id,
|
||||
actor=default_user,
|
||||
summary_search="alpha",
|
||||
)
|
||||
result_ids = [c.id for c in results]
|
||||
assert to_delete.id not in result_ids
|
||||
assert len(results) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conversation_isolation_by_agent(conversation_manager, server: SyncServer, sarah_agent, charles_agent, default_user):
|
||||
"""Test that conversations are isolated by agent."""
|
||||
|
||||
Reference in New Issue
Block a user