Skip to content

Commit 4886aa9

Browse files
author
Kevin Kantesaria
committed
feat(slack-bot): add escalation workflows, fix feedback/streaming bugs, refactor overthink
Signed-off-by: Kevin Kantesaria <kkantesaria@splunk.com>
1 parent 1d2a8c8 commit 4886aa9

File tree

7 files changed

+656
-97
lines changed

7 files changed

+656
-97
lines changed

ai_platform_engineering/integrations/slack_bot/app.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from utils.session_manager import SessionManager
2727
from utils.langfuse_client import FeedbackClient
2828
from utils.scoring import submit_feedback_score
29+
from utils.config_models import get_escalation_config
2930

3031
app = App(token=os.environ.get("SLACK_INTEGRATION_BOT_TOKEN", os.environ.get("SLACK_BOT_TOKEN", "")))
3132
APP_NAME = os.environ.get("SLACK_INTEGRATION_APP_NAME", os.environ.get("APP_NAME", "CAIPE"))
@@ -147,7 +148,13 @@ def handle_mention(event, say, client):
147148
session_manager.clear_skipped(thread_ts)
148149

149150
if is_humble_followup:
150-
mention_prompt = config.defaults.humble_followup_prompt
151+
# Use per-channel followup prompt from overthink config, fall back to global default
152+
followup = (
153+
channel_config.qanda.overthink.followup_prompt
154+
or channel_config.ai_alerts.overthink.followup_prompt
155+
or config.defaults.humble_followup_prompt
156+
)
157+
mention_prompt = followup
151158
elif channel_config.custom_prompt:
152159
mention_prompt = channel_config.custom_prompt
153160
else:
@@ -164,6 +171,9 @@ def handle_mention(event, say, client):
164171

165172
team_id = event.get("team")
166173

174+
default_config = channel_config.default if channel_config.default else {}
175+
esc_config = get_escalation_config(default_config)
176+
167177
result = ai.stream_a2a_response(
168178
a2a_client=a2a_client,
169179
slack_client=client,
@@ -175,6 +185,7 @@ def handle_mention(event, say, client):
175185
context_id=context_id,
176186
metadata=request_metadata if request_metadata else None,
177187
session_manager=session_manager,
188+
escalation_config=esc_config,
178189
)
179190

180191
if isinstance(result, dict) and result.get("retry_needed"):
@@ -261,6 +272,8 @@ def handle_qanda_message(event, say, client):
261272
final_message = f"The user email is {user_email}\n\n{final_message}"
262273
request_metadata["user_email"] = user_email
263274

275+
esc_config = get_escalation_config(default_config)
276+
264277
result = ai.stream_a2a_response(
265278
a2a_client=a2a_client,
266279
slack_client=client,
@@ -272,7 +285,8 @@ def handle_qanda_message(event, say, client):
272285
context_id=context_id,
273286
metadata=request_metadata,
274287
session_manager=session_manager,
275-
overthink_mode=channel_config.qanda.overthink,
288+
overthink_config=channel_config.qanda.overthink if channel_config.qanda.overthink.enabled else None,
289+
escalation_config=esc_config,
276290
)
277291

278292
if isinstance(result, dict) and result.get("skipped"):
@@ -481,7 +495,8 @@ def handle_message_events(body, say, client):
481495
if not default_config or not isinstance(default_config, dict):
482496
raise ValueError(f"Channel {channel_id} is missing required 'default' config")
483497

484-
ai.handle_ai_alert_processing(
498+
alerts_overthink = channel_config.ai_alerts.overthink
499+
result = ai.handle_ai_alert_processing(
485500
a2a_client,
486501
client,
487502
event,
@@ -490,8 +505,15 @@ def handle_message_events(body, say, client):
490505
default_config,
491506
session_manager,
492507
custom_prompt=channel_config.ai_alerts.custom_prompt,
508+
overthink_config=alerts_overthink if alerts_overthink.enabled else None,
493509
)
494510

511+
if isinstance(result, dict) and result.get("skipped"):
512+
reason = result.get("reason", "unknown")
513+
alert_ts = event.get("ts", "unknown")
514+
logger.info(f"[{alert_ts}] Overthink: skipped alert processing ({reason})")
515+
session_manager.set_skipped(alert_ts, True)
516+
495517

496518
# =============================================================================
497519
# HITL (Human-in-the-Loop) Form Action Handler
@@ -678,6 +700,84 @@ def handle_caipe_retry(ack, body, client):
678700
logger.exception(f"Error handling retry: {e}")
679701

680702

703+
# =============================================================================
704+
# Escalation Action Handlers
705+
# =============================================================================
706+
@app.action("caipe_escalation_get_help")
707+
def handle_escalation_get_help(ack, body, client):
708+
ack()
709+
try:
710+
from utils.escalation import execute_escalation
711+
712+
user_id = body.get("user", {}).get("id")
713+
action = body.get("actions", [{}])[0]
714+
parts = action.get("value", "").split("|")
715+
channel_id = parts[0] if len(parts) > 0 else None
716+
thread_ts = parts[1] if len(parts) > 1 else None
717+
if not channel_id or not thread_ts:
718+
return
719+
720+
# Track escalation in feedback
721+
submit_feedback_score(
722+
thread_ts=thread_ts, user_id=user_id, channel_id=channel_id,
723+
feedback_value="escalation_requested", slack_client=client,
724+
session_manager=session_manager, config=config,
725+
feedback_client=feedback_client,
726+
)
727+
728+
client.chat_postEphemeral(
729+
channel=channel_id, user=user_id, thread_ts=thread_ts,
730+
text="Got it! Connecting you with a human...",
731+
)
732+
733+
# Get escalation config for this channel
734+
channel_config = config.channels.get(channel_id)
735+
if not channel_config:
736+
return
737+
esc_config = get_escalation_config(channel_config.default or {})
738+
if not esc_config:
739+
return
740+
741+
# Determine the parent message ts (root of thread)
742+
message = body.get("message", {})
743+
parent_ts = message.get("thread_ts") or thread_ts
744+
745+
execute_escalation(
746+
slack_client=client, a2a_client=a2a_client,
747+
channel_id=channel_id, thread_ts=thread_ts,
748+
parent_ts=parent_ts, user_id=user_id,
749+
escalation_config=esc_config,
750+
)
751+
except Exception as e:
752+
logger.exception(f"Error handling escalation: {e}")
753+
754+
755+
@app.action("caipe_delete_message")
756+
def handle_delete_message(ack, body, client):
757+
ack()
758+
try:
759+
user_id = body.get("user", {}).get("id")
760+
channel_id = body.get("channel", {}).get("id")
761+
message = body.get("message", {})
762+
message_ts = message.get("ts")
763+
thread_ts = message.get("thread_ts") or message_ts
764+
765+
if not channel_id or not message_ts:
766+
return
767+
768+
submit_feedback_score(
769+
thread_ts=thread_ts, user_id=user_id, channel_id=channel_id,
770+
feedback_value="message_deleted", slack_client=client,
771+
session_manager=session_manager, config=config,
772+
feedback_client=feedback_client,
773+
)
774+
775+
client.chat_delete(channel=channel_id, ts=message_ts)
776+
logger.info(f"[{thread_ts}] Message {message_ts} deleted by <@{user_id}>")
777+
except Exception as e:
778+
logger.exception(f"Error handling message delete: {e}")
779+
780+
681781
def _open_feedback_modal(ack, body, client, feedback_type):
682782
ack()
683783
try:

ai_platform_engineering/integrations/slack_bot/tests/test_config.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import pytest
22

3-
from ai_platform_engineering.integrations.slack_bot.utils.config_models import Config
3+
from ai_platform_engineering.integrations.slack_bot.utils.config_models import (
4+
Config,
5+
EscalationConfig,
6+
OverthinkConfig,
7+
get_escalation_config,
8+
)
49

510

611
class TestSilenceEnv:
@@ -131,3 +136,109 @@ def test_config_mutual_exclusivity_validation(self, monkeypatch):
131136
)
132137
with pytest.raises(Exception, match="Cannot enable both"):
133138
Config.from_env()
139+
140+
def test_config_with_escalation(self, monkeypatch):
141+
monkeypatch.setenv(
142+
"SLACK_INTEGRATION_BOT_CONFIG",
143+
"""
144+
C123:
145+
name: "#test-channel"
146+
ai_enabled: true
147+
qanda:
148+
enabled: false
149+
ai_alerts:
150+
enabled: false
151+
default:
152+
project_key: TEST
153+
escalation:
154+
victorops:
155+
enabled: true
156+
team: platform-oncall
157+
users:
158+
- U987654
159+
emoji:
160+
enabled: true
161+
name: eyes
162+
delete_admins:
163+
- U111111
164+
""",
165+
)
166+
cfg = Config.from_env()
167+
esc = get_escalation_config(cfg.channels["C123"].default)
168+
assert esc is not None
169+
assert esc.victorops.enabled is True
170+
assert esc.victorops.team == "platform-oncall"
171+
assert esc.users == ["U987654"]
172+
assert esc.emoji.name == "eyes"
173+
assert esc.delete_admins == ["U111111"]
174+
175+
def test_get_escalation_config_returns_none_when_absent(self):
176+
assert get_escalation_config({"project_key": "TEST"}) is None
177+
178+
179+
class TestOverthinkConfig:
180+
def test_overthink_config_with_custom_markers(self, monkeypatch):
181+
monkeypatch.setenv(
182+
"SLACK_INTEGRATION_BOT_CONFIG",
183+
"""
184+
C123:
185+
name: "#test-channel"
186+
ai_enabled: true
187+
qanda:
188+
enabled: true
189+
overthink:
190+
enabled: true
191+
skip_markers: ["NO_ACTION", "IRRELEVANT"]
192+
pass_marker: "ACTIONABLE"
193+
custom_prompt: "Custom overthink: {message_text}"
194+
followup_prompt: "Custom followup: {message_text}"
195+
ai_alerts:
196+
enabled: true
197+
overthink:
198+
enabled: true
199+
skip_markers: ["SKIP"]
200+
pass_marker: "PROCESS"
201+
default:
202+
project_key: TEST
203+
""",
204+
)
205+
cfg = Config.from_env()
206+
qanda = cfg.channels["C123"].qanda
207+
assert qanda.overthink.enabled is True
208+
assert qanda.overthink.skip_markers == ["NO_ACTION", "IRRELEVANT"]
209+
assert qanda.overthink.pass_marker == "ACTIONABLE"
210+
assert qanda.overthink.custom_prompt == "Custom overthink: {message_text}"
211+
assert qanda.overthink.followup_prompt == "Custom followup: {message_text}"
212+
213+
alerts = cfg.channels["C123"].ai_alerts
214+
assert alerts.overthink.enabled is True
215+
assert alerts.overthink.skip_markers == ["SKIP"]
216+
assert alerts.overthink.pass_marker == "PROCESS"
217+
218+
def test_overthink_defaults_when_enabled_minimal(self, monkeypatch):
219+
monkeypatch.setenv(
220+
"SLACK_INTEGRATION_BOT_CONFIG",
221+
"""
222+
C123:
223+
name: "#test-channel"
224+
ai_enabled: true
225+
qanda:
226+
enabled: true
227+
overthink:
228+
enabled: true
229+
ai_alerts:
230+
enabled: false
231+
default:
232+
project_key: TEST
233+
""",
234+
)
235+
cfg = Config.from_env()
236+
cfg.apply_defaults_to_channels()
237+
qanda = cfg.channels["C123"].qanda
238+
assert qanda.overthink.enabled is True
239+
assert qanda.overthink.skip_markers == ["DEFER", "LOW_CONFIDENCE"]
240+
assert qanda.overthink.pass_marker == "CONFIDENCE: HIGH"
241+
# apply_defaults sets the default overthink prompt
242+
assert qanda.custom_prompt == cfg.defaults.overthink_qanda_prompt
243+
# apply_defaults sets the default followup prompt
244+
assert qanda.overthink.followup_prompt == cfg.defaults.humble_followup_prompt

0 commit comments

Comments
 (0)