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
5 changes: 5 additions & 0 deletions src/dispatch/ai/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Cache duration for AI-generated read-in summaries (in seconds)
READ_IN_SUMMARY_CACHE_DURATION = 120 # 2 minutes

# Tactical report generation reference in Slack
TACTICAL_REPORT_SLACK_ACTION = "tactical_report_genai"
5 changes: 1 addition & 4 deletions src/dispatch/ai/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging

from dispatch.ai.constants import READ_IN_SUMMARY_CACHE_DURATION
from dispatch.plugins.dispatch_slack.models import IncidentSubjects
import tiktoken
from sqlalchemy.orm import aliased, Session
Expand All @@ -26,10 +27,6 @@

log = logging.getLogger(__name__)

# Cache duration for AI-generated read-in summaries (in seconds)
READ_IN_SUMMARY_CACHE_DURATION = 120 # 2 minutes


def get_model_token_limit(model_name: str, buffer_percentage: float = 0.05) -> int:
"""
Returns the maximum token limit for a given LLM model with a safety buffer.
Expand Down
214 changes: 182 additions & 32 deletions src/dispatch/plugins/dispatch_slack/incident/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Section,
UsersSelect,
)
from dispatch.ai.constants import TACTICAL_REPORT_SLACK_ACTION
from slack_bolt import Ack, BoltContext, BoltRequest, Respond
from slack_sdk.errors import SlackApiError
from slack_sdk.web.client import WebClient
Expand Down Expand Up @@ -2052,6 +2053,71 @@ def handle_engage_oncall_submission_event(
)


def tactical_report_modal(
context: BoltContext,
conditions: str,
actions: str,
needs: str,
genai_loading: bool = False,
):
"""
Reusable skeleton for auto-populating the fields of the tactical report modal.
"""
blocks = [
Input(
label="Conditions",
element=PlainTextInput(
placeholder="Current incident conditions", initial_value=conditions, multiline=True
),
block_id=ReportTacticalBlockIds.conditions,
),
Input(
label="Actions",
element=PlainTextInput(
placeholder="Current incident actions", initial_value=actions, multiline=True
),
block_id=ReportTacticalBlockIds.actions,
),
Input(
label="Needs",
element=PlainTextInput(
placeholder="Current incident needs", initial_value=needs, multiline=True
),
block_id=ReportTacticalBlockIds.needs,
),
]

if genai_loading:
blocks.append(
Section(
text=MarkdownText(text=":hourglass_flowing_sand: This may take a moment. Be sure to verify all information before relying on it!")
)
)
else:
blocks.append(
Actions(
elements=[
Button(
text=":sparkles: Draft with GenAI",
action_id=TACTICAL_REPORT_SLACK_ACTION,
style="primary",
value=context["subject"].json()
)
]
))

modal = Modal(
title="Tactical Report",
blocks=blocks,
submit="Create",
close="Close",
callback_id=ReportTacticalActions.submit,
private_metadata=context["subject"].json(),
).build()

return modal


def handle_report_tactical_command(
ack: Ack,
body: dict,
Expand All @@ -2072,14 +2138,14 @@ def handle_report_tactical_command(
report_type=ReportTypes.tactical_report,
)

conditions = actions = needs = None
conditions = actions = needs = ""
if tactical_report:
conditions = tactical_report.details.get("conditions")
actions = tactical_report.details.get("actions")
needs = tactical_report.details.get("needs")

incident = incident_service.get(db_session=db_session, incident_id=int(context["subject"].id))
outstanding_actions = "" if actions is None else actions
outstanding_actions = "" if actions == "" else actions
if incident.tasks:
outstanding_actions += "\n\nOutstanding Incident Tasks:\n".join(
[
Expand All @@ -2092,40 +2158,124 @@ def handle_report_tactical_command(
if len(outstanding_actions):
actions = outstanding_actions

blocks = [
Input(
label="Conditions",
element=PlainTextInput(
placeholder="Current incident conditions", initial_value=conditions, multiline=True
),
block_id=ReportTacticalBlockIds.conditions,
),
Input(
label="Actions",
element=PlainTextInput(
placeholder="Current incident actions", initial_value=actions, multiline=True
),
block_id=ReportTacticalBlockIds.actions,
),
Input(
label="Needs",
element=PlainTextInput(
placeholder="Current incident needs", initial_value=needs, multiline=True
),
block_id=ReportTacticalBlockIds.needs,
),
]

modal = Modal(
title="Tactical Report",
blocks=blocks,
submit="Create",
modal = tactical_report_modal(
context,
conditions,
actions,
needs,
genai_loading=False
)

client.views_open(trigger_id=body["trigger_id"], view=modal)



@app.action(TACTICAL_REPORT_SLACK_ACTION,
middleware=[button_context_middleware, configuration_middleware, db_middleware, user_middleware]
)
def handle_tactical_report_draft_with_genai(
ack: Ack,
body: dict,
client: WebClient,
context: BoltContext,
user: DispatchUser
):
ack()

client.views_update(
view_id=body['view']['id'],
view=tactical_report_modal(
context,
conditions="Drafting...",
actions="Drafting...",
needs="Drafting...",
genai_loading=True,
)
)

db_session = context['db_session']
incident_id = context['subject'].id

incident = incident_service.get(
db_session=db_session,
incident_id=incident_id
)

if not incident:
log.error(f"Unable to retrieve incident with id {incident_id} to generate tactical report via Slack")

client.views_update(
view_id=body['view']['id'],
view=Modal(
title="Tactical Report",
blocks=[
Section(
text=MarkdownText(text=f":exclamation: Unable to retrieve incident with id {incident_id}. Please contact your Dispatch admin.")
)
],
close="Close",
callback_id=ReportTacticalActions.submit,
private_metadata=context["subject"].json(),
).build()
).build()
)

client.views_update(
view_id=body['view']['id'],
view=tactical_report_modal(
context=context,
conditions='',
actions='',
needs='',
genai_loading=False,
error_message=f"Unable to locate incident with id {id}. Please contact your Dispatch admins"
))
return

draft_report = ai_service.generate_tactical_report(
db_session=db_session,
project=incident.project,
incident=incident,
important_reaction=context['config'].timeline_event_reaction
)

tactical_report = draft_report.tactical_report
if not tactical_report:
error_message = draft_report.error_message if draft_report.error_message else "Unexpected error encountered generating tactical report."
log.error(error_message)
client.views_update(
view_id=body['view']['id'],
view=Modal(
title="Tactical Report",
blocks=[
Section(
text=MarkdownText(text=f":exclamation: {error_message}")
)
],
close="Close",
private_metadata=context["subject"].json(),
).build()
)
return

conditions, actions, needs = tactical_report.conditions, tactical_report.actions, tactical_report.needs
if isinstance(actions, list):
actions = '- ' + '\n- '.join(actions)
if isinstance(needs, list):
needs = '- ' + '\n-'.join(needs)

client.views_update(
view_id=body['view']['id'],
view=tactical_report_modal(
context=context,
conditions=conditions,
actions=actions,
needs=needs + '\n\nThis report was generated with AI. Please verify all information before relying on it!',
genai_loading=False
)

)


client.views_open(trigger_id=body["trigger_id"], view=modal)


def ack_report_tactical_submission_event(ack: Ack) -> None:
Expand Down
3 changes: 2 additions & 1 deletion tests/ai/test_ai_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from dispatch.ai.constants import READ_IN_SUMMARY_CACHE_DURATION
import pytest
from unittest.mock import Mock, patch

from dispatch.ai.service import generate_read_in_summary, READ_IN_SUMMARY_CACHE_DURATION, generate_tactical_report
from dispatch.ai.service import generate_read_in_summary, generate_tactical_report
from dispatch.ai.models import ReadInSummary, ReadInSummaryResponse, TacticalReport, TacticalReportResponse
from dispatch.ai.enums import AIEventSource, AIEventDescription
from dispatch.plugins.dispatch_slack.models import IncidentSubjects, CaseSubjects
Expand Down
Loading