Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit 0af1b1c

Browse files
authored
Merge branch 'master' into bugfix/jira-fallback
2 parents 7a17d9c + 624110b commit 0af1b1c

File tree

15 files changed

+387
-301
lines changed

15 files changed

+387
-301
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
16.13.0
1+
20.18.0

docker/Dockerfile

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
1717
wget \
1818
&& rm -rf /var/lib/apt/lists/*
1919

20-
RUN wget --quiet -O - https://deb.nodesource.com/setup_16.x | bash - \
20+
RUN wget --quiet -O - https://deb.nodesource.com/setup_20.x | bash - \
2121
&& apt-get install -y nodejs --no-install-recommends
2222

2323
ARG SOURCE_COMMIT
@@ -87,7 +87,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
8787
RUN echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
8888
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
8989

90-
RUN wget --quiet -O - https://deb.nodesource.com/setup_12.x | bash -
90+
RUN wget --quiet -O - https://deb.nodesource.com/setup_20.x | bash -
9191

9292
COPY --from=sdist /dist/*.whl /tmp/dist/
9393
RUN buildDeps="" \
@@ -104,7 +104,21 @@ RUN buildDeps="" \
104104
pkg-config postgresql-client-14 nodejs \
105105
&& apt-get clean \
106106
&& rm -rf /var/lib/apt/lists/* \
107-
&& npm install mjml --no-cache-dir
107+
# mjml has to be installed differently here because
108+
# after node 14, docker will install npm files at the
109+
# root directoy and fail, so we have to create a new
110+
# directory and use it for the install then copy the
111+
# files to the root directory to maintain backwards
112+
# compatibility for email generation
113+
&& mkdir -p /mjml_install \
114+
# if our workdir is /, then pushd/popd doesn't work
115+
# for the npm install. It still tries to install in /,
116+
# which npm can't do
117+
&& cd /mjml_install \
118+
&& npm install --no-cache-dir mjml \
119+
&& mv node_modules / \
120+
&& cd / \
121+
&& rm -rf /mjml_install
108122

109123
EXPOSE 8000
110124
VOLUME /var/lib/dispatch/files

src/dispatch/ai/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from dispatch.exceptions import DispatchException
2+
3+
4+
class GenAIException(DispatchException):
5+
pass

src/dispatch/ai/service.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import json
2+
import logging
3+
4+
from sqlalchemy.orm import Session
5+
6+
from dispatch.case.enums import CaseResolutionReason
7+
from dispatch.case.models import Case
8+
from dispatch.plugin import service as plugin_service
9+
from dispatch.signal import service as signal_service
10+
11+
from .exceptions import GenAIException
12+
13+
log = logging.getLogger(__name__)
14+
15+
16+
def generate_case_signal_historical_context(case: Case, db_session: Session) -> str:
17+
"""
18+
Generate historical context for a case stemming from a signal, including related cases and relevant data.
19+
20+
Args:
21+
case (Case): The case object for which historical context is being generated.
22+
db_session (Session): The database session used for querying related data.
23+
24+
Returns:
25+
str: A string containing the historical context for the case, or an error message if context generation fails.
26+
"""
27+
# we fetch the first instance id and signal
28+
(first_instance_id, first_instance_signal) = signal_service.get_instances_in_case(
29+
db_session=db_session, case_id=case.id
30+
).first()
31+
32+
signal_instance = signal_service.get_signal_instance(
33+
db_session=db_session, signal_instance_id=first_instance_id
34+
)
35+
36+
# Check if the signal instance is valid
37+
if not signal_instance:
38+
message = "Unable to generate historical context. Signal instance not found."
39+
log.warning(message)
40+
raise GenAIException(message)
41+
42+
# Check if the signal is valid
43+
if not signal_instance.signal:
44+
message = "Unable to generate historical context. Signal not found."
45+
log.warning(message)
46+
raise GenAIException(message)
47+
48+
# Check if GenAI is enabled for the signal
49+
if not signal_instance.signal.genai_enabled:
50+
message = (
51+
"Unable to generate historical context. GenAI feature not enabled for this detection."
52+
)
53+
log.warning(message)
54+
raise GenAIException(message)
55+
56+
# we fetch related cases
57+
related_cases = []
58+
for resolution_reason in CaseResolutionReason:
59+
related_cases.extend(
60+
signal_service.get_cases_for_signal_by_resolution_reason(
61+
db_session=db_session,
62+
signal_id=first_instance_signal.id,
63+
resolution_reason=resolution_reason,
64+
)
65+
.from_self() # NOTE: function deprecated in SQLAlchemy 1.4 and removed in 2.0
66+
.filter(Case.id != case.id)
67+
)
68+
69+
# we prepare historical context
70+
historical_context = []
71+
for related_case in related_cases:
72+
historical_context.append("<case>")
73+
historical_context.append(f"<case_name>{related_case.name}</case_name>")
74+
historical_context.append(f"<case_resolution>{related_case.resolution}</case_resolution")
75+
historical_context.append(
76+
f"<case_resolution_reason>{related_case.resolution_reason}</case_resolution_reason>"
77+
)
78+
historical_context.append(
79+
f"<case_alert_data>{related_case.signal_instances[0].raw}</case_alert_data>"
80+
)
81+
conversation_plugin = plugin_service.get_active_instance(
82+
db_session=db_session, project_id=case.project.id, plugin_type="conversation"
83+
)
84+
if conversation_plugin:
85+
if related_case.conversation and related_case.conversation.channel_id:
86+
# we fetch conversation replies for the related case
87+
conversation_replies = conversation_plugin.instance.get_conversation_replies(
88+
conversation_id=related_case.conversation.channel_id,
89+
thread_ts=related_case.conversation.thread_id,
90+
)
91+
for reply in conversation_replies:
92+
historical_context.append(
93+
f"<case_conversation_reply>{reply}</case_conversation_reply>"
94+
)
95+
else:
96+
log.warning(
97+
"Conversation replies not included in historical context. No conversation plugin enabled."
98+
)
99+
historical_context.append("</case>")
100+
101+
return "\n".join(historical_context)
102+
103+
104+
def generate_case_signal_summary(case: Case, db_session: Session) -> dict[str, str]:
105+
"""
106+
Generate an analysis summary of a case stemming from a signal.
107+
108+
Args:
109+
case (Case): The case object for which the analysis summary is being generated.
110+
db_session (Session): The database session used for querying related data.
111+
112+
Returns:
113+
dict: A dictionary containing the analysis summary, or an error message if the summary generation fails.
114+
"""
115+
# we generate the historical context
116+
try:
117+
historical_context = generate_case_signal_historical_context(
118+
case=case, db_session=db_session
119+
)
120+
except GenAIException as e:
121+
log.warning(f"Error generating GenAI historical context for {case.name}: {str(e)}")
122+
raise e
123+
124+
# we fetch the artificial intelligence plugin
125+
genai_plugin = plugin_service.get_active_instance(
126+
db_session=db_session, project_id=case.project.id, plugin_type="artificial-intelligence"
127+
)
128+
129+
# we check if the artificial intelligence plugin is enabled
130+
if not genai_plugin:
131+
message = (
132+
"Unable to generate GenAI signal analysis. No artificial-intelligence plugin enabled."
133+
)
134+
log.warning(message)
135+
raise GenAIException(message)
136+
137+
# we fetch the first instance id and signal
138+
(first_instance_id, first_instance_signal) = signal_service.get_instances_in_case(
139+
db_session=db_session, case_id=case.id
140+
).first()
141+
142+
signal_instance = signal_service.get_signal_instance(
143+
db_session=db_session, signal_instance_id=first_instance_id
144+
)
145+
146+
# Check if the signal instance is valid
147+
if not signal_instance:
148+
message = "Unable to generate GenAI signal analysis. Signal instance not found."
149+
log.warning(message)
150+
raise GenAIException(message)
151+
152+
# Check if the signal is valid
153+
if not signal_instance.signal:
154+
message = "Unable to generate GenAI signal analysis. Signal not found."
155+
log.warning(message)
156+
raise GenAIException(message)
157+
158+
# Check if GenAI is enabled for the signal
159+
if not signal_instance.signal.genai_enabled:
160+
message = f"Unable to generate GenAI signal analysis. GenAI feature not enabled for {signal_instance.signal.name}."
161+
log.warning(message)
162+
raise GenAIException(message)
163+
164+
# we check if the signal has a prompt defined
165+
if not signal_instance.signal.genai_prompt:
166+
message = f"Unable to generate GenAI signal analysis. No GenAI prompt defined for {signal_instance.signal.name}."
167+
log.warning(message)
168+
raise GenAIException(message)
169+
170+
# we generate the analysis
171+
response = genai_plugin.instance.chat_completion(
172+
prompt=f"""
173+
174+
<prompt>
175+
{signal_instance.signal.genai_prompt}
176+
</prompt>
177+
178+
<current_event>
179+
{str(signal_instance.raw)}
180+
</current_event>
181+
182+
<runbook>
183+
{signal_instance.signal.runbook}
184+
</runbook>
185+
186+
<historical_context>
187+
{historical_context}
188+
</historical_context>
189+
190+
"""
191+
)
192+
193+
try:
194+
summary = json.loads(
195+
response["choices"][0]["message"]["content"]
196+
.replace("```json", "")
197+
.replace("```", "")
198+
.strip()
199+
)
200+
201+
# we check if the summary is empty
202+
if not summary:
203+
message = "Unable to generate GenAI signal analysis. We received an empty response from the artificial-intelligence plugin."
204+
log.warning(message)
205+
raise GenAIException(message)
206+
207+
return summary
208+
except json.JSONDecodeError as e:
209+
message = "Unable to generate GenAI signal analysis. Error decoding response from the artificial-intelligence plugin."
210+
log.warning(message)
211+
raise GenAIException(message) from e

src/dispatch/case/flows.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ def case_auto_close_flow(case: Case, db_session: Session):
211211
db_session=db_session,
212212
)
213213

214+
if case.conversation and case.has_thread:
215+
# we update the case conversation
216+
update_conversation(case=case, db_session=db_session)
217+
214218

215219
def case_new_create_flow(
216220
*,

src/dispatch/incident/flows.py

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,57 @@
11
import logging
2-
32
from datetime import datetime
43
from typing import Optional
54

65
from sqlalchemy.orm import Session
76

8-
from dispatch.case.models import Case
97
from dispatch.case import service as case_service
8+
from dispatch.case.models import Case
109
from dispatch.conference import flows as conference_flows
1110
from dispatch.conversation import flows as conversation_flows
1211
from dispatch.database.core import resolve_attr
1312
from dispatch.decorators import background_task
1413
from dispatch.document import flows as document_flows
1514
from dispatch.document.models import Document
16-
from dispatch.enums import DocumentResourceTypes
17-
from dispatch.enums import Visibility, EventType
15+
from dispatch.enums import DocumentResourceTypes, EventType, Visibility
1816
from dispatch.event import service as event_service
1917
from dispatch.group import flows as group_flows
20-
from dispatch.group.enums import GroupType, GroupAction
18+
from dispatch.group.enums import GroupAction, GroupType
2119
from dispatch.incident import service as incident_service
2220
from dispatch.incident.models import IncidentRead
2321
from dispatch.incident_cost import service as incident_cost_service
2422
from dispatch.individual import service as individual_service
23+
from dispatch.individual.models import IndividualContact
2524
from dispatch.participant import flows as participant_flows
2625
from dispatch.participant import service as participant_service
2726
from dispatch.participant.models import Participant
28-
from dispatch.individual.models import IndividualContact
29-
from dispatch.team.models import TeamContact
3027
from dispatch.participant_role import flows as participant_role_flows
3128
from dispatch.participant_role.models import ParticipantRoleType
3229
from dispatch.plugin import service as plugin_service
3330
from dispatch.report.enums import ReportTypes
3431
from dispatch.report.messaging import send_incident_report_reminder
3532
from dispatch.service import service as service_service
3633
from dispatch.storage import flows as storage_flows
34+
from dispatch.tag.flows import check_for_tag_change
3735
from dispatch.task.enums import TaskStatus
36+
from dispatch.team.models import TeamContact
3837
from dispatch.ticket import flows as ticket_flows
39-
from dispatch.tag.flows import check_for_tag_change
4038

4139
from .messaging import (
42-
# get_suggested_document_items,
40+
bulk_participant_announcement_message,
4341
send_incident_closed_information_review_reminder,
4442
send_incident_commander_readded_notification,
4543
send_incident_created_notifications,
4644
send_incident_management_help_tips_message,
4745
send_incident_new_role_assigned_notification,
4846
send_incident_open_tasks_ephemeral_message,
49-
send_participant_announcement_message,
50-
bulk_participant_announcement_message,
5147
send_incident_rating_feedback_message,
5248
send_incident_review_document_notification,
53-
# send_incident_suggested_reading_messages,
5449
send_incident_update_notifications,
5550
send_incident_welcome_participant_messages,
51+
send_participant_announcement_message,
5652
)
5753
from .models import Incident, IncidentStatus
5854

59-
6055
log = logging.getLogger(__name__)
6156

6257

@@ -315,13 +310,6 @@ def incident_create_resources(
315310
# we send the welcome messages to the participant
316311
send_incident_welcome_participant_messages(user_email, incident, db_session)
317312

318-
# NOTE: Temporarily disabled until an issue with the Dispatch resolver plugin is resolved
319-
# we send a suggested reading message to the participant
320-
# suggested_document_items = get_suggested_document_items(incident, db_session)
321-
# send_incident_suggested_reading_messages(
322-
# incident, suggested_document_items, user_email, db_session
323-
# )
324-
325313
bulk_participant_announcement_message(
326314
participant_emails=user_emails,
327315
subject=incident,
@@ -1051,13 +1039,6 @@ def incident_add_or_reactivate_participant_flow(
10511039
# we send the welcome messages to the participant
10521040
send_incident_welcome_participant_messages(user_email, incident, db_session)
10531041

1054-
# NOTE: Temporarily disabled until an issue with the Dispatch resolver plugin is resolved
1055-
# we send a suggested reading message to the participant
1056-
# suggested_document_items = get_suggested_document_items(incident, db_session)
1057-
# send_incident_suggested_reading_messages(
1058-
# incident, suggested_document_items, user_email, db_session
1059-
# )
1060-
10611042
return participant
10621043

10631044

0 commit comments

Comments
 (0)