Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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 backend/apps/slack/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Slack app admin."""

from .bot_interaction import BotInteractionAdmin
from .conversation import ConversationAdmin
from .event import EventAdmin
from .member import MemberAdmin
Expand Down
87 changes: 87 additions & 0 deletions backend/apps/slack/admin/bot_interaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Bot interaction admin configuration."""

from django.contrib import admin

from apps.slack.models.bot_interaction import BotInteraction


class BotInteractionAdmin(admin.ModelAdmin):
"""Admin for BotInteraction model."""

fieldsets = (
(
"Interaction",
{
"fields": (
"channel_id",
"user_id",
"user_message",
"bot_response",
)
},
),
(
"Classification",
{
"fields": (
"intent_category",
"confidence_score",
"tokens_used",
)
},
),
(
"Feedback",
{
"fields": (
"thumbs_up",
"slack_reply_ts",
)
},
),
)
list_display = (
"channel_id",
"user_id",
"short_message",
"intent_category",
"thumbs_up",
"tokens_used",
"nest_created_at",
)
list_filter = (
"thumbs_up",
"intent_category",
)
readonly_fields = (
"channel_id",
"user_id",
"user_message",
"bot_response",
"slack_reply_ts",
"tokens_used",
"nest_created_at",
"nest_updated_at",
)
search_fields = (
"channel_id",
"user_id",
"user_message",
"intent_category",
)

MESSAGE_TRUNCATE_LENGTH = 60

@admin.display(description="Message")
def short_message(self, obj):
"""Return truncated user message for list display."""
if not obj.user_message:
return ""
return (
obj.user_message[: self.MESSAGE_TRUNCATE_LENGTH] + "…"
if len(obj.user_message) > self.MESSAGE_TRUNCATE_LENGTH
else obj.user_message
)


admin.site.register(BotInteraction, BotInteractionAdmin)
5 changes: 3 additions & 2 deletions backend/apps/slack/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def configure_slack_events():
owasp_community,
project_nest,
)

from apps.slack.events.reaction_added import bot_feedback # noqa: F401 ← ADD THIS LINE

if SlackConfig.app:
EventBase.configure_events()
EventBase.configure_events()
22 changes: 22 additions & 0 deletions backend/apps/slack/events/member_joined_channel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
def configure_slack_events():
"""Configure Slack events after Django apps are ready."""
from apps.slack.apps import SlackConfig
from apps.slack.events import (
app_home_opened,
app_mention,
message_posted,
team_join,
url_verification,
)
from apps.slack.events.event import EventBase
from apps.slack.events.member_joined_channel import (
catch_all,
contribute,
gsoc,
owasp_community,
project_nest,
)
from apps.slack.events.reaction_added import bot_feedback # noqa: F401 ← ADD THIS LINE

if SlackConfig.app:
EventBase.configure_events()
1 change: 1 addition & 0 deletions backend/apps/slack/events/reaction_added/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Slack reaction_added event handlers."""
66 changes: 66 additions & 0 deletions backend/apps/slack/events/reaction_added/bot_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Slack reaction_added event handler for bot reply feedback."""

import logging

from apps.slack.events.event import EventBase
from apps.slack.models.bot_interaction import BotInteraction

logger = logging.getLogger(__name__)

THUMBS_UP = "thumbsup"
THUMBS_DOWN = "thumbsdown"
FEEDBACK_REACTIONS = {THUMBS_UP, THUMBS_DOWN}


class BotFeedback(EventBase):
"""Records 👍 / 👎 reactions on NestBot replies."""

event_type = "reaction_added"

def handle_event(self, event, client):
"""Handle a reaction_added event.

Checks whether the reacted-to message ts matches a BotInteraction
slack_reply_ts. If so, records the feedback. Reactions on non-bot
messages are silently ignored.

Args:
event (dict): The Slack reaction_added event payload.
client: The Slack WebClient instance (unused but required by base).

"""
reaction = event.get("reaction", "")
if reaction not in FEEDBACK_REACTIONS:
return

item = event.get("item", {})
if item.get("type") != "message":
return

reply_ts = item.get("ts", "")
if not reply_ts:
return

channel_id = item.get("channel", "")
if not channel_id:
return

interaction = (
BotInteraction.objects.filter(
slack_reply_ts=reply_ts,
channel_id=channel_id,
)
.order_by("-nest_created_at")
.first()
)
if interaction is None:
return

interaction.thumbs_up = reaction == THUMBS_UP
interaction.save(update_fields=["thumbs_up", "nest_updated_at"])

logger.info(
"Feedback recorded for BotInteraction pk=%s: %s",
interaction.pk,
"👍" if interaction.thumbs_up else "👎",
)
77 changes: 77 additions & 0 deletions backend/apps/slack/migrations/0023_bot_interaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("slack", "0022_workspace_invite_link_last_alert_message_ts_and_more"),
]

operations = [
migrations.CreateModel(
name="BotInteraction",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("nest_created_at", models.DateTimeField(auto_now_add=True)),
("nest_updated_at", models.DateTimeField(auto_now=True)),
(
"channel_id",
models.CharField(max_length=64, verbose_name="Channel ID"),
),
(
"user_id",
models.CharField(max_length=64, verbose_name="User ID"),
),
("user_message", models.TextField(verbose_name="User message")),
("bot_response", models.TextField(verbose_name="Bot response")),
(
"intent_category",
models.CharField(
blank=True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: slack_reply_ts should not be optional because it is the key used to attach 👍/👎 feedback. Allowing empty values stores interactions that can never be correlated back from reactions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/apps/slack/migrations/0023_bot_interaction.py, line 40:

<comment>`slack_reply_ts` should not be optional because it is the key used to attach 👍/👎 feedback. Allowing empty values stores interactions that can never be correlated back from reactions.</comment>

<file context>
@@ -0,0 +1,83 @@
+                (
+                    "intent_category",
+                    models.CharField(
+                        blank=True,
+                        default="",
+                        max_length=64,
</file context>

default="",
max_length=64,
verbose_name="Intent category",
),
),
(
"confidence_score",
models.FloatField(blank=True, null=True, verbose_name="Confidence score"),
),
(
"thumbs_up",
models.BooleanField(
blank=True,
help_text="True = 👍, False = 👎, None = no reaction yet.",
null=True,
verbose_name="Thumbs up",
),
),
(
"tokens_used",
models.PositiveIntegerField(default=0, verbose_name="Tokens used"),
),
(
"slack_reply_ts",
models.CharField(
blank=True,
db_index=True,
default="",
help_text="Slack message ts of the bot reply. Used to match reaction_added events.",
max_length=32,
verbose_name="Slack reply timestamp",
),
),
],
options={
"verbose_name_plural": "Bot Interactions",
"db_table": "slack_bot_interactions",
},
),
]
3 changes: 2 additions & 1 deletion backend/apps/slack/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .bot_interaction import BotInteraction
from .conversation import Conversation
from .event import Event
from .member import Member
from .message import Message
from .workspace import Workspace
from .workspace import Workspace
64 changes: 64 additions & 0 deletions backend/apps/slack/models/bot_interaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Slack app bot interaction model."""

from django.db import models

from apps.common.models import TimestampedModel


class BotInteraction(TimestampedModel):
"""Tracks AI-generated replies and their user feedback."""

class Meta:
"""Model options."""

db_table = "slack_bot_interactions"
verbose_name_plural = "Bot Interactions"

channel_id = models.CharField(
verbose_name="Channel ID",
max_length=64,
)
user_id = models.CharField(
verbose_name="User ID",
max_length=64,
)
user_message = models.TextField(
verbose_name="User message",
)
bot_response = models.TextField(
verbose_name="Bot response",
)
intent_category = models.CharField(
verbose_name="Intent category",
max_length=64,
blank=True,
default="",
)
confidence_score = models.FloatField(
verbose_name="Confidence score",
null=True,
blank=True,
)
thumbs_up = models.BooleanField(
verbose_name="Thumbs up",
null=True,
blank=True,
help_text="True = 👍, False = 👎, None = no reaction yet.",
)
tokens_used = models.PositiveIntegerField(
verbose_name="Tokens used",
default=0,
)
slack_reply_ts = models.CharField(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
verbose_name="Slack reply timestamp",
max_length=32,
blank=True,
default="",
db_index=True,
help_text="Slack message ts of the bot reply. Used to match reaction_added events.",
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def __str__(self):
"""Human readable representation."""
feedback = {True: "👍", False: "👎", None: "—"}[self.thumbs_up]
return f"[{self.channel_id}] {self.user_message[:50]!r} → {feedback}"
20 changes: 19 additions & 1 deletion backend/apps/slack/services/message_auto_reply.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import logging

from django.db import DatabaseError
from django_rq import job
from slack_sdk.errors import SlackApiError

from apps.slack.apps import SlackConfig
from apps.slack.common.handlers.ai import get_blocks, process_ai_query
from apps.slack.models import Message
from apps.slack.models.bot_interaction import BotInteraction

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,9 +47,25 @@ def generate_ai_reply_if_unanswered(message_id: int):
if not ai_response_text:
return

client.chat_postMessage(
response = client.chat_postMessage(
channel=message.conversation.slack_channel_id,
blocks=get_blocks(ai_response_text),
text=ai_response_text,
thread_ts=message.slack_message_id,
)

# Log the interaction so reaction_added can match 👍/👎 back to this reply.
reply_ts = response.get("ts", "") if response else ""
try:
BotInteraction.objects.create(
channel_id=message.conversation.slack_channel_id,
user_id=message.raw_data.get("user", ""),
user_message=message.text,
bot_response=ai_response_text,
slack_reply_ts=reply_ts,
)
except DatabaseError:
logger.exception(
"Failed to persist BotInteraction after sending Slack reply",
extra={"channel_id": message.conversation.slack_channel_id, "reply_ts": reply_ts},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for reaction_added events."""
Loading
Loading