Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ You can override their values if you wish to do so. Included below are their des
| `/dispatch-notifications-group` | Opens a modal to edit the notifications group. |
| `/dispatch-update-participant` | Opens a modal to update participant metadata. |
| `/dispatch-create-task` | Opens a modal to create an incident task. |
| `/dispatch-create-case` | Opens a modal to create a case. |
| `/dispatch-create-case` | Opens a modal to create a case. |
| `/dispatch-summary` | If allowed for this case/incident type, will create an AI-generated read-in summary. |

### Contact Information Resolver Plugin

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Adds new Slack command for read-in summary generation
Revision ID: aa87efd3d6c1
Revises: f63ad392dbbf
Create Date: 2025-07-11 10:02:39.819258
"""

from alembic import op
from pydantic import SecretStr, ValidationError
from pydantic.json import pydantic_encoder

from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, Session
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy_utils import StringEncryptedType
from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine
from dispatch.config import config, DISPATCH_ENCRYPTION_KEY


# revision identifiers, used by Alembic.
revision = "aa87efd3d6c1"
down_revision = "f63ad392dbbf"
branch_labels = None
depends_on = None

Base = declarative_base()


def show_secrets_encoder(obj):
if isinstance(obj, SecretStr):
return obj.get_secret_value()
else:
return pydantic_encoder(obj)


def migrate_config(instances, slug, config):
for instance in instances:
if slug == instance.plugin.slug:
instance.configuration = config


class Plugin(Base):
__tablename__ = "plugin"
__table_args__ = {"schema": "dispatch_core"}
id = Column(Integer, primary_key=True)
slug = Column(String, unique=True)


class PluginInstance(Base):
__tablename__ = "plugin_instance"
id = Column(Integer, primary_key=True)
_configuration = Column(
StringEncryptedType(key=str(DISPATCH_ENCRYPTION_KEY), engine=AesEngine, padding="pkcs5")
)
plugin_id = Column(Integer, ForeignKey(Plugin.id))
plugin = relationship(Plugin, backref="instances")

@hybrid_property
def configuration(self):
"""Property that correctly returns a plugins configuration object."""
pass

@configuration.setter
def configuration(self, configuration):
"""Property that correctly sets a plugins configuration object."""
if configuration:
self._configuration = configuration.json(encoder=show_secrets_encoder)


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
from dispatch.plugins.dispatch_slack.config import SlackConversationConfiguration

bind = op.get_bind()
session = Session(bind=bind)

instances = session.query(PluginInstance).all()

# Slash commands
SLACK_COMMAND_SUMMARY_SLUG = config("SLACK_COMMAND_SUMMARY_SLUG", default="/dispatch-summary")

try:
slack_conversation_config = SlackConversationConfiguration(
slack_command_summary=SLACK_COMMAND_SUMMARY_SLUG,
)

migrate_config(instances, "slack-conversation", slack_conversation_config)

except ValidationError:
print(
"Skipping automatic migration of slack plugin credentials, if you are using the slack plugin manually migrate credentials."
)

session.commit()
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions src/dispatch/plugins/dispatch_slack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,8 @@ class SlackConversationConfiguration(SlackConfiguration):
title="Engage User Command String",
description="Defines the string used to engage a user via MFA prompt. Must match what is defined in Slack.",
)
slack_command_summary: str = Field(
"/dispatch-summary",
title="Generate Summary Command String",
description="Defines the string used to generate a summary. Must match what is defined in Slack.",
)
160 changes: 157 additions & 3 deletions src/dispatch/plugins/dispatch_slack/incident/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ def configure(config):
)
app.command(config.slack_command_create_task, middleware=middleware)(handle_create_task_command)

app.command(
config.slack_command_summary,
middleware=[
message_context_middleware,
subject_middleware,
configuration_middleware,
user_middleware,
],
)(handle_summary_command)

app.event(
event="reaction_added",
matchers=[is_target_reaction(config.timeline_event_reaction)],
Expand Down Expand Up @@ -1175,10 +1185,10 @@ def handle_member_joined_channel(
time.sleep(1)

generate_read_in_summary = False
subject_type = "incident"
subject_type = context["subject"].type
project = None

if context["subject"].type == IncidentSubjects.incident:
if subject_type == IncidentSubjects.incident:
participant = incident_flows.incident_add_or_reactivate_participant_flow(
user_email=user.email, incident_id=int(context["subject"].id), db_session=db_session
)
Expand Down Expand Up @@ -1237,7 +1247,7 @@ def handle_member_joined_channel(
db_session.add(participant)
db_session.commit()

if context["subject"].type == CaseSubjects.case:
if subject_type == CaseSubjects.case:
subject_type = "case"
case = case_service.get(db_session=db_session, case_id=int(context["subject"].id))

Expand All @@ -1258,6 +1268,8 @@ def handle_member_joined_channel(
generate_read_in_summary = getattr(case.case_type, "generate_read_in_summary", False)
if case.visibility == Visibility.restricted:
generate_read_in_summary = False
if not case.dedicated_channel:
generate_read_in_summary = False

participant.user_conversation_id = context["user_id"]

Expand Down Expand Up @@ -3005,3 +3017,145 @@ def handle_remind_again_select_action(
respond(
text=message, response_type="ephemeral", replace_original=False, delete_original=False
)


def handle_summary_command(
ack: Ack,
body: dict,
client: WebClient,
context: BoltContext,
db_session: Session,
user: DispatchUser,
) -> None:
"""Handles the summary command to generate a read-in summary."""
ack()

try:
if context["subject"].type == IncidentSubjects.incident:
incident = incident_service.get(
db_session=db_session, incident_id=int(context["subject"].id)
)
project = incident.project
subject_type = "incident"

if incident.visibility == Visibility.restricted:
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: Cannot generate summary for restricted incidents.",
)
return

if not incident.incident_type.generate_read_in_summary:
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: Read-in summaries are not enabled for this incident type.",
)
return

elif context["subject"].type == CaseSubjects.case:
case = case_service.get(db_session=db_session, case_id=int(context["subject"].id))
project = case.project
subject_type = "case"

if case.visibility == Visibility.restricted:
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: Cannot generate summary for restricted cases.",
)
return

if not case.case_type.generate_read_in_summary:
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: Read-in summaries are not enabled for this case type.",
)
return

if not case.dedicated_channel:
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: Read-in summaries are only available for cases with a dedicated channel.",
)
return
else:
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: Error: Unable to determine subject type for summary generation.",
)
return

# All validations passed
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":hourglass_flowing_sand: Generating read-in summary... This may take a moment.",
)

summary_response = ai_service.generate_read_in_summary(
db_session=db_session,
subject=context["subject"],
project=project,
channel_id=context["channel_id"],
important_reaction=context["config"].timeline_event_reaction,
participant_email=user.email,
)

if summary_response and summary_response.summary:
blocks = create_read_in_summary_blocks(summary_response.summary)
blocks.append(
Context(
elements=[
MarkdownText(
text="NOTE: The block above was AI-generated and may contain errors or inaccuracies. Please verify the information before relying on it."
)
]
).build()
)

dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=f"Here is a summary of what has happened so far in this {subject_type}",
blocks=blocks,
)
elif summary_response and summary_response.error_message:
log.warning(f"Failed to generate read-in summary: {summary_response.error_message}")

dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: Unable to generate summary at this time. Please try again later.",
)
else:
# No summary generated
dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: No summary could be generated. There may not be enough information available.",
)

except Exception as e:
log.error(f"Error generating summary: {e}")

dispatch_slack_service.send_ephemeral_message(
client=client,
conversation_id=context["channel_id"],
user_id=context["user_id"],
text=":x: An error occurred while generating the summary. Please try again later.",
)
2 changes: 1 addition & 1 deletion tests/plugins/test_dispatch_slack_incident_interactive.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

def test_configure():
"""Test that we can configure the plugin."""
from dispatch.plugins.dispatch_slack.incident.interactive import (
Expand Down Expand Up @@ -32,6 +31,7 @@ def test_configure():
"slack_command_list_workflow": "/dispatch-list-workflows",
"slack_command_list_tasks": "/dispatch-list-tasks",
"slack_command_create_task": "/dispatch-create-task",
"slack_command_summary": "/dispatch-summary",
}
)

Expand Down
Loading