Skip to content

Commit 583b5e3

Browse files
authored
feat(genai): elevates GenAI feature to its own service library (Netflix#5416)
* feat(genai): elevates GenAI feature to its own service library * fixes
1 parent 3638445 commit 583b5e3

File tree

4 files changed

+259
-149
lines changed

4 files changed

+259
-149
lines changed

src/dispatch/ai/exceptions.py

+5
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

+211
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

0 commit comments

Comments
 (0)