-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
223 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# AI Driven Slack Support | ||
|
||
This automation uses a slack bot to receive requests for help as a bot mention. Once a request for help is received, the subject of the request is inferred using Google's Gemini AI. The appropiate person, per a predetemined table of expertize that reside in a Google Doc, is mention. The person can then `!take` the request, and later `!resolve` it. If no one picks up the request for a configurable duration, the automation will remind the person that a request is pending. | ||
|
||
For example, given this expertise table: | ||
|
||
``` | ||
| A | B | C | ||
---+---------+-----------+-------------- | ||
1 | Itay | U12345678 | cats,dogs | ||
2 | Haim | U87654321 | russian | ||
``` | ||
|
||
This would happen: | ||
|
||
![demo](./demo.png) | ||
|
||
# Deploy | ||
|
||
Requirements: | ||
|
||
- Slack integration is set up. See https://docs.autokitteh.com/tutorials/new_connections/slack. | ||
- Google integration is set up. See TODO. | ||
|
||
First apply the manifest: | ||
|
||
``` | ||
$ ak manifest apply autokitteh.yaml | ||
``` | ||
|
||
Then initialize the google and slack connections. This will authenticate them to the desired Slack workspace and Google account. | ||
|
||
``` | ||
$ ak connection init slack_support/myslack | ||
$ ak connection init slack_support/mygsheets | ||
``` | ||
|
||
Now acquire a Gemini API key from google. Go to https://ai.google.dev/gemini-api/docs/api-key and follow the instructions. | ||
Set the variable in autokitteh: | ||
|
||
``` | ||
$ ak env set --env slack_support/default --secret GEMINI_API_KEY <api-key> | ||
``` | ||
|
||
Now create your Google Sheet containing the schedule, it shoud look like this: | ||
|
||
``` | ||
| A | B | C | ||
---+---------+-----------+-------------- | ||
1 | Gizmo | U12345678 | topic1,topic2 | ||
2 | George | U87654321 | topic3 | ||
``` | ||
|
||
Set the sheet ID in the autokitteh environment: | ||
|
||
``` | ||
$ ak env set --env slack_support/default DIRECTORY_GOOGLE_SHEET_ID <google-sheet-id> | ||
``` | ||
|
||
Now you are ready to roll. Deploy your project: | ||
|
||
``` | ||
$ ak deploy --project slack_support --dir . | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
version: v1 | ||
|
||
project: | ||
name: slack_support | ||
vars: | ||
- # Google Sheet ID for a sheet that contains a mapping between users and the | ||
# the topics they can suport. | ||
# Expected google sheet structure: | ||
# | A | B | C | ||
# --+---------+-----------+-------------- | ||
# 1 | Gizmo | U12345678 | topic1,topic2 | ||
# 2 | George | U87654321 | topic3 | ||
name: DIRECTORY_GOOGLE_SHEET_ID | ||
- # A gemini key must be acquired from https://aistudio.google.com/app/apikey. | ||
name: GEMINI_API_KEY | ||
- # Time in minutes to wait for the issue to be picked up before | ||
# reminder. | ||
name: HELP_REQUEST_TIMEOUT_MINUTES | ||
value: "10" | ||
connections: | ||
- name: myslack | ||
integration: slack | ||
- name: mygsheets | ||
integration: googlesheets | ||
triggers: | ||
- name: slack_app_mention | ||
connection: myslack | ||
event_type: app_mention | ||
call: main.py:on_slack_mention |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import os | ||
from collections import namedtuple | ||
from autokitteh.google import google_sheets_client | ||
|
||
DIRECTORY_GOOGLE_SHEET_ID = os.getenv("DIRECTORY_GOOGLE_SHEET_ID") | ||
|
||
gsheets = google_sheets_client("mygsheets") | ||
|
||
|
||
Person = namedtuple("Person", ["name", "slack_id", "topics"]) | ||
|
||
|
||
def load() -> dict[str, list[Person]]: # topic -> list of people | ||
vs = ( | ||
gsheets.spreadsheets() | ||
.values() | ||
.get(spreadsheetId=DIRECTORY_GOOGLE_SHEET_ID, range="A1:C100") | ||
.execute() | ||
.get("values", []) | ||
) | ||
|
||
ppl = [Person(v[0], v[1], v[2].split(",")) for v in vs] | ||
|
||
topics = set([topic for person in ppl for topic in person.topics]) | ||
|
||
return {topic: [person for person in ppl if topic in person.topics] for topic in topics} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import os | ||
import json | ||
|
||
import google.generativeai as genai | ||
|
||
# How do i know if this is "picklable" or not? It has no return value? Do I need to run this every time? | ||
# Is this running in an activity? | ||
genai.configure(api_key=os.getenv("GEMINI_API_KEY")) | ||
|
||
# Make this run every time outside of an activity if it's in global scope? | ||
model = genai.GenerativeModel( | ||
"gemini-1.5-flash", | ||
generation_config={"response_mime_type": "application/json"}, | ||
) | ||
|
||
|
||
def extract_topic(text: str, topics: set[str]) -> str: | ||
prompt = f"""Topics: {', '.join(topics)} | ||
Is the following text a request for help with one of these topics? | ||
Example responses: | ||
If a request for help and a topic from the list: {{"help": true, "topic": "cats" }} | ||
If a request for help and topic is not from the list: {{"help": true, "topic": None }} | ||
If not a request for help: {{"help": false}} | ||
Text to analyze: | ||
{text}""" | ||
|
||
resp = json.loads(model.generate_content(prompt).text) | ||
return resp.get("help"), resp.get("topic") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import os | ||
from datetime import datetime | ||
|
||
import autokitteh | ||
from autokitteh.slack import slack_client | ||
|
||
import gemini | ||
import directory | ||
|
||
HELP_REQUEST_TIMEOUT_MINUTES = int(os.getenv("HELP_REQUEST_TIMEOUT_MINUTES")) | ||
|
||
slack_client = slack_client("myslack") | ||
|
||
|
||
def on_slack_mention(event): | ||
def send(text): | ||
"""Helper function to just post some text back in the same thread.""" | ||
slack_client.chat_postMessage( | ||
channel=event.data.channel, | ||
thread_ts=event.data.ts, | ||
text=text, | ||
) | ||
|
||
# prints are used for logging, and can be seen in the console output. | ||
print(f"sent: '{text}'") | ||
|
||
topics_to_people = directory.load() | ||
|
||
help, topic = gemini.extract_topic(event.data.text, topics_to_people.keys()) | ||
if not help: | ||
return | ||
|
||
people = topics_to_people.get(topic) | ||
if not people: | ||
send(f"Sorry, I don't know who to ask about {topic}.") | ||
return | ||
|
||
mentions = ", ".join(f"<@{p.slack_id}>" for p in people) | ||
|
||
send(f"""People who can help are: {mentions}. | ||
Responders: please reply in this thread with `!take` or `!resolve`. | ||
If not taken or resolved, I will remind you in {HELP_REQUEST_TIMEOUT_MINUTES}m. | ||
""") | ||
|
||
# From this point on we are interested in any message that is added to the thread. | ||
# Further below we'll consume the messages and act on them using `next_event`. | ||
s = autokitteh.subscribe( | ||
"myslack", | ||
f'data.type == "message" && data.thread_ts == "{event.data.ts}" && data.text.startsWith("!")', | ||
) | ||
|
||
taken_by = None | ||
start_time = datetime.now() | ||
|
||
while True: | ||
msg = autokitteh.next_event(s, timeout=60) | ||
|
||
if not msg: # timeout | ||
dt = datetime.now().total_seconds() - start_time.total_seconds() | ||
print(f"timeout, dt={dt}") | ||
|
||
if not taken_by and dt >= HELP_REQUEST_TIMEOUT_MINUTES * 60: | ||
send(f"Reminder: {mentions}, please respond.") | ||
start_time = datetime.now() | ||
continue | ||
|
||
cmd = msg.text.strip()[1:] | ||
if cmd == "resolve": | ||
send("Issue is now resolved.") | ||
# this effectively ends the workflow. | ||
return | ||
if cmd == "take": | ||
taken_by = msg.user | ||
send(f"Thanks <@{msg.user}>, you've taken this issue.") |