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:
github-actions[bot]
2026-02-20 15:28:25 -08:00
committed by Caren Thomas
parent 9c8589a687
commit ba67621e1b
5 changed files with 267 additions and 8 deletions

View File

@@ -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."""

View File

@@ -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."""