From f7d186f3d99b081c2bd33db18fb949b56197c1b3 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Tue, 8 Oct 2024 10:39:55 -0700 Subject: [PATCH 1/3] Adds genai_analysis column to case model --- src/dispatch/case/models.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dispatch/case/models.py b/src/dispatch/case/models.py index 4c7eb938d6bb..0559664e6312 100644 --- a/src/dispatch/case/models.py +++ b/src/dispatch/case/models.py @@ -14,17 +14,18 @@ Table, UniqueConstraint, ) +from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship from sqlalchemy_utils import TSVectorType, observes +from dispatch.case.priority.models import CasePriorityBase, CasePriorityCreate, CasePriorityRead +from dispatch.case.severity.models import CaseSeverityBase, CaseSeverityCreate, CaseSeverityRead +from dispatch.case.type.models import CaseTypeBase, CaseTypeCreate, CaseTypeRead from dispatch.case_cost.models import ( CaseCostRead, CaseCostUpdate, ) -from dispatch.case.priority.models import CasePriorityBase, CasePriorityCreate, CasePriorityRead -from dispatch.case.severity.models import CaseSeverityBase, CaseSeverityCreate, CaseSeverityRead -from dispatch.case.type.models import CaseTypeBase, CaseTypeCreate, CaseTypeRead from dispatch.conversation.models import ConversationRead from dispatch.database.core import Base from dispatch.document.models import Document, DocumentRead @@ -37,10 +38,10 @@ from dispatch.models import ( DispatchBase, NameStr, + Pagination, PrimaryKey, ProjectMixin, TimeStampMixin, - Pagination, ) from dispatch.participant.models import ( Participant, @@ -48,7 +49,6 @@ ParticipantReadMinimal, ParticipantUpdate, ) - from dispatch.storage.models import StorageRead from dispatch.tag.models import TagRead from dispatch.ticket.models import TicketRead @@ -88,13 +88,12 @@ class Case(Base, TimeStampMixin, ProjectMixin): visibility = Column(String, default=Visibility.open, nullable=False) participants_team = Column(String) participants_location = Column(String) - reported_at = Column(DateTime, default=datetime.utcnow) triage_at = Column(DateTime) escalated_at = Column(DateTime) closed_at = Column(DateTime) - dedicated_channel = Column(Boolean, default=False) + genai_analysis = Column(JSONB, default={}, nullable=False, server_default="{}") search_vector = Column( TSVectorType( @@ -302,20 +301,21 @@ class CaseRead(CaseBase): case_severity: CaseSeverityRead case_type: CaseTypeRead closed_at: Optional[datetime] = None + conversation: Optional[ConversationRead] = None created_at: Optional[datetime] = None documents: Optional[List[DocumentRead]] = [] duplicates: Optional[List[CaseReadMinimal]] = [] escalated_at: Optional[datetime] = None events: Optional[List[EventRead]] = [] + genai_analysis: Optional[dict[str, Any]] = {} groups: Optional[List[GroupRead]] = [] incidents: Optional[List[IncidentReadMinimal]] = [] - conversation: Optional[ConversationRead] = None name: Optional[NameStr] + participants: Optional[List[ParticipantRead]] = [] project: ProjectRead related: Optional[List[CaseReadMinimal]] = [] - reporter: Optional[ParticipantRead] reported_at: Optional[datetime] = None - participants: Optional[List[ParticipantRead]] = [] + reporter: Optional[ParticipantRead] signal_instances: Optional[List[SignalInstanceRead]] = [] storage: Optional[StorageRead] = None tags: Optional[List[TagRead]] = [] From d4bff839a51ce8ef97b5434bdd6fb1d0c61cea8b Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Tue, 8 Oct 2024 12:35:32 -0700 Subject: [PATCH 2/3] adds logic to store summary as json --- .../versions/2024-10-08_b057c079c2d5.py | 37 ++++++++++ .../plugins/dispatch_slack/case/messages.py | 70 ++++++++++++++----- src/dispatch/plugins/dispatch_slack/plugin.py | 9 ++- 3 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 src/dispatch/database/revisions/tenant/versions/2024-10-08_b057c079c2d5.py diff --git a/src/dispatch/database/revisions/tenant/versions/2024-10-08_b057c079c2d5.py b/src/dispatch/database/revisions/tenant/versions/2024-10-08_b057c079c2d5.py new file mode 100644 index 000000000000..1b5129437373 --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-10-08_b057c079c2d5.py @@ -0,0 +1,37 @@ +"""Adds genai_analysis to case model + +Revision ID: b057c079c2d5 +Revises: f5107ce190fc +Create Date: 2024-10-08 10:38:39.668625 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "b057c079c2d5" +down_revision = "f5107ce190fc" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "case", + sa.Column( + "genai_analysis", + postgresql.JSONB(astext_type=sa.Text()), + server_default="{}", + nullable=False, + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("case", "genai_analysis") + # ### end Alembic commands ### diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index 03851c8b4c0d..8d3bb8dc6e70 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -1,5 +1,6 @@ +import json import logging -from typing import NamedTuple +from typing import NamedTuple, Tuple from blockkit import ( Actions, @@ -292,9 +293,37 @@ def create_action_buttons_message( return Message(blocks=signal_metadata_blocks).build()["blocks"] +def json_to_slack_format(json_message: dict[str, str]) -> str: + """ + Converts a JSON dictionary to Slack markup format. + + Args: + json_dict (dict): The JSON dictionary to convert. + + Returns: + str: A string formatted with Slack markup. + """ + slack_message = "" + for key, value in json_message.items(): + slack_message += f"*{key}*\n{value}\n\n" + return slack_message.strip() + + def create_genai_signal_message_metadata_blocks( - signal_metadata_blocks: list[Block], message: str + signal_metadata_blocks: list[Block], message: str | dict[str, str] ) -> list[Block]: + """ + Appends a GenAI signal analysis section to the signal metadata blocks. + + Args: + signal_metadata_blocks (list[Block]): The list of existing signal metadata blocks. + message (str | dict[str, str]): The GenAI analysis message, either as a string or a dictionary. + + Returns: + list[Block]: The updated list of signal metadata blocks with the GenAI analysis section appended. + """ + if isinstance(message, dict): + message = json_to_slack_format(message) signal_metadata_blocks.append( Section(text=f":magic_wand: *GenAI Alert Analysis*\n\n{message}"), ) @@ -308,23 +337,19 @@ def create_genai_signal_analysis_message( db_session: Session, client: WebClient, config: SlackConversationConfiguration, -) -> list[Block]: +) -> Tuple[str, list[Block]]: """ - Creates a signal analysis using a generative AI plugin. - - This function generates an analysis for a given case by leveraging historical context and - a generative AI plugin. It fetches related cases, their resolutions, and relevant Slack - messages to provide a comprehensive analysis. + Creates a GenAI signal analysis message for a given case. Args: - case (Case): The case object containing details to be included in the analysis. - channel_id (str): The ID of the Slack channel where the analysis will be sent. - db_session (Session): The database session to use for querying signal instances and related cases. - client (WebClient): The Slack WebClient to fetch threaded messages. + case (Case): The case object containing details to be included in the message. + channel_id (str): The ID of the Slack channel where the message will be sent. + db_session (Session): The database session to use for querying signal instances. + client (WebClient): The Slack WebClient to use for interacting with the Slack API. config (SlackConversationConfiguration): The Slack conversation configuration. Returns: - list[Block]: A list of Block objects representing the structure of the Slack message. + Tuple[str, list[Block]]: A tuple containing the GenAI analysis message and a list of Block objects representing the structure of the Slack message. """ signal_metadata_blocks: list[Block] = [] @@ -341,7 +366,7 @@ def create_genai_signal_analysis_message( if not signal_instance.signal.genai_enabled: message = "Unable to generate GenAI signal analysis. GenAI feature not enabled for this detection." log.warning(message) - return create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) + return message, create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) # we fetch related cases related_cases = [] @@ -388,7 +413,9 @@ def create_genai_signal_analysis_message( f"Unable to generate GenAI signal analysis. Error fetching Slack messages for case {related_case.name}: {e}" ) message = "Unable to generate GenAI signal analysis. Error fetching Slack messages." - return create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) + return message, create_genai_signal_message_metadata_blocks( + signal_metadata_blocks, message + ) historical_context.append("") @@ -405,13 +432,13 @@ def create_genai_signal_analysis_message( "Unable to generate GenAI signal analysis. No artificial-intelligence plugin enabled." ) log.warning(message) - return create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) + return message, create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) # we check if the GenAI plugin has a prompt if not signal_instance.signal.genai_prompt: message = f"Unable to generate GenAI signal analysis. No GenAI prompt defined for {signal_instance.signal.name}" log.warning(message) - return create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) + return message, create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) # we generate the analysis response = genai_plugin.instance.chat_completion( @@ -435,14 +462,19 @@ def create_genai_signal_analysis_message( """ ) - message = response["choices"][0]["message"]["content"] + message = json.loads( + response["choices"][0]["message"]["content"] + .replace("```json", "") + .replace("```", "") + .strip() + ) # we check if the response is empty if not message: message = "Unable to generate GenAI signal analysis. We received an empty response from the artificial-intelligence plugin." log.warning(message) - return create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) + return message, create_genai_signal_message_metadata_blocks(signal_metadata_blocks, message) def create_signal_engagement_message( diff --git a/src/dispatch/plugins/dispatch_slack/plugin.py b/src/dispatch/plugins/dispatch_slack/plugin.py index 8682958380dc..7f8af02c5140 100644 --- a/src/dispatch/plugins/dispatch_slack/plugin.py +++ b/src/dispatch/plugins/dispatch_slack/plugin.py @@ -100,18 +100,21 @@ def create_threaded(self, case: Case, conversation_id: str, db_session: Session) # we try to generate a GenAI signal analysis message try: - if message := create_genai_signal_analysis_message( + message, message_blocks = create_genai_signal_analysis_message( case=case, channel_id=conversation_id, db_session=db_session, client=client, config=self.configuration, - ): + ) + if message: + case.genai_analysis = message + signal_response = send_message( client=client, conversation_id=conversation_id, ts=response_timestamp, - blocks=message, + blocks=message_blocks, ) except Exception as e: logger.exception(f"Error generating GenAI signal analysis message: {e}") From 149ac97c86121eae5b73a1b9cc77887384714210 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Tue, 8 Oct 2024 12:41:22 -0700 Subject: [PATCH 3/3] check if we have message blocks before sending them --- src/dispatch/plugins/dispatch_slack/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dispatch/plugins/dispatch_slack/plugin.py b/src/dispatch/plugins/dispatch_slack/plugin.py index 7f8af02c5140..dd93e2c22c7a 100644 --- a/src/dispatch/plugins/dispatch_slack/plugin.py +++ b/src/dispatch/plugins/dispatch_slack/plugin.py @@ -110,6 +110,7 @@ def create_threaded(self, case: Case, conversation_id: str, db_session: Session) if message: case.genai_analysis = message + if message_blocks: signal_response = send_message( client=client, conversation_id=conversation_id,