Skip to content

Commit

Permalink
work
Browse files Browse the repository at this point in the history
  • Loading branch information
itayd committed Aug 21, 2024
1 parent d63d8a3 commit 3751e5c
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ projects for:
|[Pull Request Review Reminder (Purrr)](./purrr/) | Streamline code reviews and cut down turnaround time to merge pull requests | GitHub ↔ Slack |
|[ReviewKitteh](./reviewkitteh/) | Monitor pull requests, and meow at random people | GitHub, Google Sheets, Slack |
| 🐍 [Task chain](./task_chain/) | Run a sequence of tasks with fault tolerance | Slack |
| 🐍 [Slack Support](./slack_support/) | Categorize slack support requests using AI and make sure appropiate people handle them | Slack, Google Sheets, Gemini |

> [!NOTE]
> 🐍 = Python implementation, ⭐ = Starlark implementation.
64 changes: 64 additions & 0 deletions slack_support/README.md
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 .
```
29 changes: 29 additions & 0 deletions slack_support/autokitteh.yaml
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
Binary file added slack_support/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions slack_support/directory.py
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}
29 changes: 29 additions & 0 deletions slack_support/gemini.py
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")
74 changes: 74 additions & 0 deletions slack_support/main.py
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.")

0 comments on commit 3751e5c

Please sign in to comment.