Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Merged
130 changes: 101 additions & 29 deletions src/dispatch/plugins/dispatch_slack/case/messages.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,49 @@
import logging
from collections import defaultdict
from typing import NamedTuple


from blockkit import (
Actions,
Button,
Context,
Divider,
Message,
Section,
Divider,
)
from blockkit.surfaces import Block
from slack_sdk.errors import SlackApiError
from slack_sdk.web.client import WebClient
from sqlalchemy.orm import Session

from dispatch.config import DISPATCH_UI_URL
from dispatch.case.enums import CaseStatus
from dispatch.case.models import Case
from dispatch.config import DISPATCH_UI_URL
from dispatch.entity import service as entity_service
from dispatch.entity.models import Entity
from dispatch.entity_type.models import EntityType
from dispatch.entity import service as entity_service
from dispatch.messaging.strings import CASE_STATUS_DESCRIPTIONS, CASE_VISIBILITY_DESCRIPTIONS
from dispatch.plugin import service as plugin_service
from dispatch.plugins.dispatch_slack.case.enums import (
CaseNotificationActions,
SignalEngagementActions,
SignalNotificationActions,
)
from dispatch.plugins.dispatch_slack.config import MAX_SECTION_TEXT_LENGTH
from dispatch.plugins.dispatch_slack.models import (
CaseSubjects,
EngagementMetadata,
SubjectMetadata,
SignalSubjects,
SubjectMetadata,
)
from dispatch.plugins.dispatch_slack.case.enums import (
CaseNotificationActions,
SignalNotificationActions,
SignalEngagementActions,
)
from dispatch.signal import service as signal_service
from dispatch.signal.enums import SignalEngagementStatus
from dispatch.signal.models import (
Signal,
SignalEngagement,
SignalInstance,
assoc_signal_instance_entities,
)
from dispatch.signal.enums import SignalEngagementStatus
from dispatch.plugins.dispatch_slack.config import MAX_SECTION_TEXT_LENGTH

log = logging.getLogger(__name__)


def map_priority_color(color: str) -> str:
Expand Down Expand Up @@ -144,12 +149,6 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]:
style="primary",
value=button_metadata,
),
Button(
text="Create Channel",
action_id=CaseNotificationActions.migrate,
style="primary",
value=button_metadata,
),
Button(
text="Edit",
action_id=CaseNotificationActions.edit,
Expand All @@ -162,6 +161,12 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]:
style="danger",
value=button_metadata,
),
Button(
text="Create Channel",
action_id=CaseNotificationActions.migrate,
style="primary",
value=button_metadata,
),
]
if case.status == CaseStatus.new:
action_buttons.insert(
Expand All @@ -186,16 +191,9 @@ class EntityGroup(NamedTuple):
def create_signal_messages(case_id: int, channel_id: str, db_session: Session) -> list[Message]:
"""Creates the signal instance message."""

signal_instances_query = (
db_session.query(SignalInstance, Signal)
.join(Signal)
.with_entities(SignalInstance.id, Signal)
.filter(SignalInstance.case_id == case_id)
.order_by(SignalInstance.created_at)
)

(first_instance_id, first_instance_signal) = signal_instances_query.first()
num_of_instances = signal_instances_query.count()
instances = signal_service.get_instances_in_case(db_session=db_session, case_id=case_id)
(first_instance_id, first_instance_signal) = instances.first()
num_of_instances = instances.count()

organization_slug = first_instance_signal.project.organization.slug
project_id = first_instance_signal.project.id
Expand Down Expand Up @@ -298,6 +296,80 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) -
return Message(blocks=signal_metadata_blocks).build()["blocks"]


def create_genai_signal_summary(
case: Case,
channel_id: str,
db_session: Session,
client: WebClient,
) -> list[Block]:
"""Creates enhanced signal messages with historical context."""
signal_metadata_blocks: list[Block] = []

instances = signal_service.get_instances_in_case(db_session=db_session, case_id=case.id)
(first_instance_id, first_instance_signal) = instances.first()

related_cases = signal_service.get_cases_for_signal(
db_session=db_session, signal_id=first_instance_signal.id
).filter(Case.id != case.id)

# Prepare historical context
historical_context = []
for related_case in related_cases:
historical_context.append(f"Case: {related_case.name}")
historical_context.append(f"Resolution: {related_case.resolution}")
historical_context.append(f"Resolution Reason: {related_case.resolution_reason}")

# Fetch Slack messages for the related case
if related_case.conversation and related_case.conversation.channel_id:
try:
# Fetch threaded messages
thread_messages = client.conversations_replies(
channel=related_case.conversation.channel_id,
ts=related_case.conversation.thread_id,
)

# Add relevant messages to the context (e.g., first 5 messages)
for message in thread_messages["messages"][:5]:
historical_context.append(f"Slack Message: {message['text']}")
except SlackApiError as e:
log.error(f"Error fetching Slack messages for case {related_case.name}: {e}")

historical_context.append("---")

historical_context_str = "\n".join(historical_context)

signal_instance = signal_service.get_signal_instance(
db_session=db_session, signal_instance_id=first_instance_id
)

genai_plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=case.project.id, plugin_type="artificial-intelligence"
)
if not genai_plugin:
return signal_metadata_blocks

response = genai_plugin.instance.chat_completion(
prompt=f"""{first_instance_signal.prompt}

Current Event:
{str(signal_instance.raw)}

Historical Context:
{historical_context_str}

Runbook:
{first_instance_signal.runbook}
"""
)
message = response["choices"][0]["message"]["content"]

signal_metadata_blocks.append(
Context(elements=[message]),
)

return Message(blocks=signal_metadata_blocks).build()["blocks"]


def create_signal_engagement_message(
case: Case,
channel_id: str,
Expand Down
32 changes: 24 additions & 8 deletions src/dispatch/plugins/dispatch_slack/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

from blockkit import Message
from blockkit.surfaces import Block
import io
import json
import logging
from typing import List, Optional, Any
from typing import Any, List, Optional

from blockkit import Message
from blockkit.surfaces import Block
from slack_sdk.errors import SlackApiError
from sqlalchemy.orm import Session

Expand All @@ -30,11 +31,11 @@
from dispatch.signal.enums import SignalEngagementStatus
from dispatch.signal.models import SignalEngagement, SignalInstance


from .case.messages import (
create_case_message,
create_signal_messages,
create_genai_signal_summary,
create_signal_engagement_message,
create_signal_messages,
)
from .endpoints import router as slack_event_router
from .enums import SlackAPIErrorCode
Expand All @@ -57,13 +58,12 @@
resolve_user,
send_ephemeral_message,
send_message,
set_conversation_topic,
set_conversation_description,
set_conversation_topic,
unarchive_conversation,
update_message,
)


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -121,6 +121,19 @@ def create_threaded(self, case: Case, conversation_id: str, db_session: Session)
logger.exception(f"Error uploading alert JSON to the Case thread: {e}")
except Exception as e:
logger.exception(f"Error uploading alert JSON to the Case thread: {e}")

message = create_genai_signal_summary(
case=case,
channel_id=conversation_id,
db_session=db_session,
client=client,
)
send_message(
client=client,
conversation_id=conversation_id,
ts=case.signal_thread_ts,
blocks=message,
)
db_session.commit()
return response

Expand Down Expand Up @@ -156,7 +169,10 @@ def create_engagement_threaded(
engagement_status=engagement_status,
)
return send_message(
client=client, conversation_id=conversation_id, blocks=blocks, ts=thread_id
client=client,
conversation_id=conversation_id,
blocks=blocks,
ts=thread_id,
)

def update_thread(self, case: Case, conversation_id: str, ts: str):
Expand Down
30 changes: 25 additions & 5 deletions src/dispatch/signal/service.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import logging
import json
import logging
import uuid
from datetime import datetime, timedelta, timezone
from typing import Optional, Union

from fastapi import HTTPException, status
from pydantic.error_wrappers import ErrorWrapper, ValidationError

from sqlalchemy import desc, asc, or_
from sqlalchemy import asc, desc, or_
from sqlalchemy.orm import Session
from sqlalchemy.orm.query import Query
from sqlalchemy.sql.expression import true

from dispatch.auth.models import DispatchUser
from dispatch.case.models import Case
from dispatch.case.priority import service as case_priority_service
from dispatch.case.type import service as case_type_service
from dispatch.case.type.models import CaseType
Expand All @@ -29,9 +30,7 @@
SignalNotDefinedException,
SignalNotIdentifiedException,
)

from .models import (
assoc_signal_entity_types,
Signal,
SignalCreate,
SignalEngagement,
Expand All @@ -46,6 +45,7 @@
SignalInstance,
SignalInstanceCreate,
SignalUpdate,
assoc_signal_entity_types,
)

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -758,3 +758,23 @@ def get_unprocessed_signal_instance_ids(session: Session) -> list[int]:
.limit(500)
.all()
)


def get_instances_in_case(db_session: Session, case_id: int) -> Query:
return (
db_session.query(SignalInstance, Signal)
.join(Signal)
.with_entities(SignalInstance.id, Signal)
.filter(SignalInstance.case_id == case_id)
.order_by(SignalInstance.created_at)
)


def get_cases_for_signal(db_session: Session, signal_id: int, limit: int = 10) -> Query:
return (
db_session.query(Case)
.join(SignalInstance)
.filter(SignalInstance.signal_id == signal_id)
.order_by(desc(Case.created_at))
.limit(limit)
)