Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added `/clear` slash command to reset conversation context in issues and merge requests
- Added changelog subagent for maintaining changelogs and release notes across any format (CHANGELOG.md, CHANGES.rst, HISTORY.md, NEWS, etc.) with automatic format detection and convention preservation
- Added code review and security audit agent skills for structured review and security guidance.
- Added support for issue labels to configure plan and execute agent behavior:
Expand Down
3 changes: 2 additions & 1 deletion daiv/slash_commands/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .clear import ClearSlashCommand
from .clone_to_topic import CloneToTopicsSlashCommand
from .help import HelpSlashCommand

__all__ = ["CloneToTopicsSlashCommand", "HelpSlashCommand"]
__all__ = ["ClearSlashCommand", "CloneToTopicsSlashCommand", "HelpSlashCommand"]
53 changes: 53 additions & 0 deletions daiv/slash_commands/actions/clear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import logging

from django.conf import settings as django_settings

from langgraph.checkpoint.postgres import PostgresSaver

from codebase.base import Scope
from core.utils import generate_uuid
from slash_commands.base import SlashCommand
from slash_commands.decorator import slash_command

logger = logging.getLogger("daiv.slash_commands")


@slash_command(command="clear", scopes=[Scope.ISSUE, Scope.MERGE_REQUEST])
class ClearSlashCommand(SlashCommand):
"""
Command to clear the conversation context by deleting the thread.
"""

description: str = "Clear the conversation context and start fresh."

async def execute_for_agent(
self, *, args: str, issue_iid: int | None = None, merge_request_id: int | None = None, **kwargs
) -> str:
"""
Execute clear command for agent middleware.

Deletes the thread associated with the current issue or merge request.

Args:
args: Additional parameters from the command (unused).
issue_iid: The issue IID (for Issue scope).
merge_request_id: The merge request ID (for Merge Request scope).

Returns:
Success or error message.
"""
if self.scope == Scope.ISSUE and issue_iid is not None:
thread_id = generate_uuid(f"{self.repo_id}:{self.scope.value}/{issue_iid}")
elif self.scope == Scope.MERGE_REQUEST and merge_request_id is not None:
thread_id = generate_uuid(f"{self.repo_id}:{self.scope.value}/{merge_request_id}")
else:
return f"The /{self.command} command is only available for issues and merge requests."

try:
with PostgresSaver.from_conn_string(django_settings.DB_URI) as checkpointer:
checkpointer.delete_thread(thread_id)
logger.info("Thread %s deleted successfully via /%s command", thread_id, self.command)
return "✅ Conversation context cleared successfully. You can start a fresh conversation now."
except Exception as e:
logger.exception("Failed to delete thread %s via /%s command", thread_id, self.command)
return f"❌ Failed to clear conversation context: {e}"
29 changes: 29 additions & 0 deletions docs/features/slash-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ graph TD
I --> J["🛠️ Execute Specific Logic"]

J --> K["📖 Help Command<br/>(show available commands)"]
J --> L["🧹 Clear Command<br/>(reset conversation)"]
J --> R["📤 Clone to Topic<br/>(clone issue to repos)"]

K --> N["💬 Posts Help Message"]
L --> O["🗑️ Deletes Thread<br/>(clears context)"]
R --> S["📋 Creates Issues in<br/>Matching Repositories"]

H --> Q["💬 Posts Error Message<br/>(suggests valid commands)"]
Expand Down Expand Up @@ -97,6 +99,32 @@ graph TD

---

### 🧹 Clear command

**Command**: `/clear`

**Purpose**: Clear the conversation context and start a fresh conversation with DAIV.

**Scopes**: Issues, Merge/Pull Requests

**Usage**: When you want to reset the conversation history and start fresh, use this command. This deletes the thread associated with the current issue or merge request, effectively clearing DAIV's memory of the conversation.

**Example**:
```
@daiv /clear
```

**Response**: DAIV replies with a confirmation that the context has been cleared successfully.

**Behavior**:

- Deletes the conversation thread for the current issue or merge request
- Clears all conversation history and context
- Allows starting a new conversation from scratch
- Useful when previous context is no longer relevant or causing confusion

---

### 📤 Clone to topic command

**Command**: `/clone-to-topic <topics>`
Expand Down Expand Up @@ -184,6 +212,7 @@ Slash commands log detailed information for troubleshooting:
Comment one of the commands below on this issue to trigger the bot:

- `@daiv /help` - Shows the help message with the available slash commands.
- `@daiv /clear` - Clear the conversation context and start fresh.
- `@daiv /clone-to-topic <topics>` - Clone this issue to all repositories matching the specified topics.
```

Expand Down
90 changes: 90 additions & 0 deletions tests/unit_tests/slash_commands/actions/test_clear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from unittest.mock import MagicMock, Mock, patch

from django.test import override_settings

from pytest import fixture

from codebase.base import Scope
from slash_commands.actions.clear import ClearSlashCommand


@fixture
def clear_slash_command_issue() -> ClearSlashCommand:
"""Set up test fixtures for issue scope."""
command = ClearSlashCommand(scope=Scope.ISSUE, repo_id="repo1", bot_username="bot")
command.client = MagicMock(current_user=MagicMock(username="bot"))
return command


@fixture
def clear_slash_command_mr() -> ClearSlashCommand:
"""Set up test fixtures for merge request scope."""
command = ClearSlashCommand(scope=Scope.MERGE_REQUEST, repo_id="repo1", bot_username="bot")
command.client = MagicMock(current_user=MagicMock(username="bot"))
return command


def test_clear_command_has_correct_attributes():
"""Test that ClearSlashCommand has the expected attributes set by decorator."""
assert hasattr(ClearSlashCommand, "command")
assert hasattr(ClearSlashCommand, "scopes")
assert ClearSlashCommand.command == "clear"
assert Scope.ISSUE in ClearSlashCommand.scopes
assert Scope.MERGE_REQUEST in ClearSlashCommand.scopes
assert Scope.GLOBAL not in ClearSlashCommand.scopes


@override_settings(DB_URI="postgresql://test:test@localhost:5432/test")
@patch("slash_commands.actions.clear.PostgresSaver")
async def test_clear_command_for_issue(mock_postgres_saver: Mock, clear_slash_command_issue: ClearSlashCommand):
"""Test that ClearSlashCommand deletes the thread for an issue."""
mock_checkpointer = MagicMock()
mock_postgres_saver.from_conn_string.return_value.__enter__.return_value = mock_checkpointer

message = await clear_slash_command_issue.execute_for_agent(args="", issue_iid=42)

# Verify the thread was deleted
mock_checkpointer.delete_thread.assert_called_once()
assert "cleared successfully" in message
assert "✅" in message


@override_settings(DB_URI="postgresql://test:test@localhost:5432/test")
@patch("slash_commands.actions.clear.PostgresSaver")
async def test_clear_command_for_merge_request(mock_postgres_saver: Mock, clear_slash_command_mr: ClearSlashCommand):
"""Test that ClearSlashCommand deletes the thread for a merge request."""
mock_checkpointer = MagicMock()
mock_postgres_saver.from_conn_string.return_value.__enter__.return_value = mock_checkpointer

message = await clear_slash_command_mr.execute_for_agent(args="", merge_request_id=123)

# Verify the thread was deleted
mock_checkpointer.delete_thread.assert_called_once()
assert "cleared successfully" in message
assert "✅" in message


@patch("slash_commands.actions.clear.PostgresSaver")
async def test_clear_command_without_issue_iid(mock_postgres_saver: Mock, clear_slash_command_issue: ClearSlashCommand):
"""Test that ClearSlashCommand returns an error when issue_iid is missing."""
message = await clear_slash_command_issue.execute_for_agent(args="")

# Verify no thread deletion attempted
mock_postgres_saver.from_conn_string.assert_not_called()
assert "only available for issues and merge requests" in message


@override_settings(DB_URI="postgresql://test:test@localhost:5432/test")
@patch("slash_commands.actions.clear.PostgresSaver")
async def test_clear_command_handles_exceptions(
mock_postgres_saver: Mock, clear_slash_command_issue: ClearSlashCommand
):
"""Test that ClearSlashCommand handles exceptions gracefully."""
mock_checkpointer = MagicMock()
mock_checkpointer.delete_thread.side_effect = Exception("Database error")
mock_postgres_saver.from_conn_string.return_value.__enter__.return_value = mock_checkpointer

message = await clear_slash_command_issue.execute_for_agent(args="", issue_iid=42)

assert "Failed to clear" in message
assert "❌" in message