Skip to content

Commit

Permalink
feat(ui): add incident summary to Reports tab (#5582)
Browse files Browse the repository at this point in the history
  • Loading branch information
whitdog47 authored Dec 6, 2024
1 parent 5ed6795 commit f2f709f
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Adds incident summary to the incident table.
Revision ID: 575ca7d954a8
Revises: 928b725d64f6
Create Date: 2024-12-05 15:05:46.932404
"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "575ca7d954a8"
down_revision = "928b725d64f6"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("incident", sa.Column("summary", sa.String(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("incident", "summary")
# ### end Alembic commands ###
3 changes: 3 additions & 0 deletions src/dispatch/incident/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,9 @@ def incident_closed_status_flow(incident: Incident, db_session=None):
# to rate and provide feedback about the incident
send_incident_rating_feedback_message(incident, db_session)

# if an AI plugin is enabled, we send the incident review doc for summary
incident_service.generate_incident_summary(incident, db_session)


def conversation_topic_dispatcher(
user_email: str,
Expand Down
5 changes: 5 additions & 0 deletions src/dispatch/incident/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ def last_executive_report(self):
notifications_group_id = Column(Integer, ForeignKey("group.id"))
notifications_group = relationship("Group", foreign_keys=[notifications_group_id])

summary = Column(String, nullable=True)

@hybrid_property
def total_cost(self):
total_cost = 0
Expand Down Expand Up @@ -323,6 +325,7 @@ class IncidentReadMinimal(IncidentBase):
reporters_location: Optional[str]
stable_at: Optional[datetime] = None
storage: Optional[StorageRead] = None
summary: Optional[str] = None
tags: Optional[List[TagRead]] = []
tasks: Optional[List[TaskReadMinimal]] = []
total_cost: Optional[float]
Expand All @@ -344,6 +347,7 @@ class IncidentUpdate(IncidentBase):
reported_at: Optional[datetime] = None
reporter: Optional[ParticipantUpdate]
stable_at: Optional[datetime] = None
summary: Optional[str] = None
tags: Optional[List[TagRead]] = []
terms: Optional[List[TermRead]] = []

Expand Down Expand Up @@ -393,6 +397,7 @@ class IncidentRead(IncidentBase):
reporters_location: Optional[str]
stable_at: Optional[datetime] = None
storage: Optional[StorageRead] = None
summary: Optional[str] = None
tags: Optional[List[TagRead]] = []
tasks: Optional[List[TaskRead]] = []
terms: Optional[List[TermRead]] = []
Expand Down
26 changes: 7 additions & 19 deletions src/dispatch/incident/scheduled.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,25 +286,13 @@ def incident_report_weekly(db_session: Session, project: Project):
if incident.visibility == Visibility.restricted:
continue
try:
pir_doc = storage_plugin.instance.get(
file_id=incident.incident_review_document.resource_id,
mime_type="text/plain",
)
prompt = f"""
Given the text of the security post-incident review document below,
provide answers to the following questions in a paragraph format.
Do not include the questions in your response.
1. What is the summary of what happened?
2. What were the overall risk(s)?
3. How were the risk(s) mitigated?
4. How was the incident resolved?
5. What are the follow-up tasks?
{pir_doc}
"""

response = ai_plugin.instance.chat_completion(prompt=prompt)
summary = response["choices"][0]["message"]["content"]
# if already summary generated, use that instead
if incident.summary:
summary = incident.summary
else:
summary = incident_service.generate_incident_summary(
db_session=db_session, incident=incident
)

item = {
"commander_fullname": incident.commander.individual.name,
Expand Down
101 changes: 86 additions & 15 deletions src/dispatch/incident/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from typing import List, Optional
from pydantic.error_wrappers import ErrorWrapper, ValidationError

from sqlalchemy.orm import Session

from dispatch.decorators import timer
from dispatch.case import service as case_service
from dispatch.database.core import SessionLocal
from dispatch.enums import Visibility
from dispatch.event import service as event_service
from dispatch.exceptions import NotFoundError
from dispatch.incident.priority import service as incident_priority_service
Expand All @@ -35,9 +37,7 @@
log = logging.getLogger(__name__)


def resolve_and_associate_role(
db_session: SessionLocal, incident: Incident, role: ParticipantRoleType
):
def resolve_and_associate_role(db_session: Session, incident: Incident, role: ParticipantRoleType):
"""For a given role type resolve which individual email should be assigned that role."""
email_address = None
service_id = None
Expand Down Expand Up @@ -65,12 +65,12 @@ def resolve_and_associate_role(


@timer
def get(*, db_session, incident_id: int) -> Optional[Incident]:
def get(*, db_session: Session, incident_id: int) -> Optional[Incident]:
"""Returns an incident based on the given id."""
return db_session.query(Incident).filter(Incident.id == incident_id).first()


def get_by_name(*, db_session, project_id: int, name: str) -> Optional[Incident]:
def get_by_name(*, db_session: Session, project_id: int, name: str) -> Optional[Incident]:
"""Returns an incident based on the given name."""
return (
db_session.query(Incident)
Expand All @@ -80,7 +80,9 @@ def get_by_name(*, db_session, project_id: int, name: str) -> Optional[Incident]
)


def get_all_open_by_incident_type(*, db_session, incident_type_id: int) -> List[Optional[Incident]]:
def get_all_open_by_incident_type(
*, db_session: Session, incident_type_id: int
) -> List[Optional[Incident]]:
"""Returns all non-closed incidents based on the given incident type."""
return (
db_session.query(Incident)
Expand All @@ -90,7 +92,9 @@ def get_all_open_by_incident_type(*, db_session, incident_type_id: int) -> List[
)


def get_by_name_or_raise(*, db_session, project_id: int, incident_in: IncidentRead) -> Incident:
def get_by_name_or_raise(
*, db_session: Session, project_id: int, incident_in: IncidentRead
) -> Incident:
"""Returns an incident based on a given name or raises ValidationError"""
incident = get_by_name(db_session=db_session, project_id=project_id, name=incident_in.name)

Expand All @@ -110,12 +114,14 @@ def get_by_name_or_raise(*, db_session, project_id: int, incident_in: IncidentRe
return incident


def get_all(*, db_session, project_id: int) -> List[Optional[Incident]]:
def get_all(*, db_session: Session, project_id: int) -> List[Optional[Incident]]:
"""Returns all incidents."""
return db_session.query(Incident).filter(Incident.project_id == project_id)


def get_all_by_status(*, db_session, status: str, project_id: int) -> List[Optional[Incident]]:
def get_all_by_status(
*, db_session: Session, status: str, project_id: int
) -> List[Optional[Incident]]:
"""Returns all incidents based on the given status."""
return (
db_session.query(Incident)
Expand All @@ -125,7 +131,7 @@ def get_all_by_status(*, db_session, status: str, project_id: int) -> List[Optio
)


def get_all_last_x_hours(*, db_session, hours: int) -> List[Optional[Incident]]:
def get_all_last_x_hours(*, db_session: Session, hours: int) -> List[Optional[Incident]]:
"""Returns all incidents in the last x hours."""
now = datetime.utcnow()
return (
Expand All @@ -134,7 +140,7 @@ def get_all_last_x_hours(*, db_session, hours: int) -> List[Optional[Incident]]:


def get_all_last_x_hours_by_status(
*, db_session, status: str, hours: int, project_id: int
*, db_session: Session, status: str, hours: int, project_id: int
) -> List[Optional[Incident]]:
"""Returns all incidents of a given status in the last x hours."""
now = datetime.utcnow()
Expand Down Expand Up @@ -167,7 +173,7 @@ def get_all_last_x_hours_by_status(
)


def create(*, db_session, incident_in: IncidentCreate) -> Incident:
def create(*, db_session: Session, incident_in: IncidentCreate) -> Incident:
"""Creates a new incident."""
project = project_service.get_by_name_or_default(
db_session=db_session, project_in=incident_in.project
Expand Down Expand Up @@ -326,7 +332,7 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident:
return incident


def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> Incident:
def update(*, db_session: Session, incident: Incident, incident_in: IncidentUpdate) -> Incident:
"""Updates an existing incident."""
incident_type = incident_type_service.get_by_name_or_default(
db_session=db_session,
Expand Down Expand Up @@ -417,7 +423,72 @@ def update(*, db_session, incident: Incident, incident_in: IncidentUpdate) -> In
return incident


def delete(*, db_session, incident_id: int):
def delete(*, db_session: Session, incident_id: int):
"""Deletes an existing incident."""
db_session.query(Incident).filter(Incident.id == incident_id).delete()
db_session.commit()


def generate_incident_summary(*, db_session: Session, incident: Incident) -> str:
"""Generates a summary of the incident."""
# Skip summary for restricted incidents
if incident.visibility == Visibility.restricted:
return "Incident summary not generated for restricted incident."

# Skip if no incident review document
if not incident.incident_review_document or not incident.incident_review_document.resource_id:
log.info(
f"Incident summary not generated for incident {incident.id}. No review document found."
)
return "Incident summary not generated. No review document found."

# Don't generate if no enabled ai plugin or storage plugin
ai_plugin = plugin_service.get_active_instance(
db_session=db_session, plugin_type="artificial-intelligence", project_id=incident.project.id
)
if not ai_plugin:
log.info(
f"Incident summary not generated for incident {incident.id}. No AI plugin enabled."
)
return "Incident summary not generated. No AI plugin enabled."

storage_plugin = plugin_service.get_active_instance(
db_session=db_session, plugin_type="storage", project_id=incident.project.id
)

if not storage_plugin:
log.info(
f"Incident summary not generated for incident {incident.id}. No storage plugin enabled."
)
return "Incident summary not generated. No storage plugin enabled."

try:
pir_doc = storage_plugin.instance.get(
file_id=incident.incident_review_document.resource_id,
mime_type="text/plain",
)
prompt = f"""
Given the text of the security post-incident review document below,
provide answers to the following questions in a paragraph format.
Do not include the questions in your response.
1. What is the summary of what happened?
2. What were the overall risk(s)?
3. How were the risk(s) mitigated?
4. How was the incident resolved?
5. What are the follow-up tasks?
{pir_doc}
"""

response = ai_plugin.instance.chat_completion(prompt=prompt)
summary = response["choices"][0]["message"]["content"]

incident.summary = summary
db_session.add(incident)
db_session.commit()

return summary

except Exception as e:
log.exception(f"Error trying to generate summary for incident {incident.id}: {e}")
return "Incident summary not generated. An error occurred."
17 changes: 16 additions & 1 deletion src/dispatch/incident/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
IncidentRead,
IncidentUpdate,
)
from .service import create, delete, get, update
from .service import create, delete, get, update, generate_incident_summary

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -497,3 +497,18 @@ def get_incident_forecast(
{"name": "Actual", "data": actual[1:]},
],
}


@router.get(
"/{incident_id}/regenerate",
summary="Regenerates incident sumamary",
dependencies=[Depends(PermissionsDependency([IncidentEventPermission]))],
)
def generate_summary(
db_session: DbSession,
current_incident: CurrentIncident,
):
return generate_incident_summary(
db_session=db_session,
incident=current_incident,
)
2 changes: 1 addition & 1 deletion src/dispatch/messaging/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class MessageType(DispatchEnum):
).strip()

INCIDENT_WEEKLY_REPORT_NO_INCIDENTS_DESCRIPTION = """
No open incidents have been closed in the last week.""".replace(
No open visibility incidents have been closed in the last week.""".replace(
"\n", " "
).strip()

Expand Down
Loading

0 comments on commit f2f709f

Please sign in to comment.