Skip to content

Commit

Permalink
Feature/submit yt issue (#3)
Browse files Browse the repository at this point in the history
* WIP

* yt sumission is working

* bot provides yt issue link now

* fix prompt and function call escription
  • Loading branch information
RageAgainstTheMachine101 authored Oct 23, 2023
1 parent df2af53 commit 2c24502
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 110 deletions.
33 changes: 33 additions & 0 deletions slack_bot/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

import contextlib
from pathlib import Path
from typing import Any


# This class represents a simple database that stores its data as files in a directory.
class DB:
def __init__(self, path: Path) -> None:
"""Initialize a DB instance."""
self.path = path

def get(self, key: str) -> str | None:
"""Get a value from the DB."""
try:
with (self.path / key).open(mode="rb") as f:
return f.read().decode("utf-8")
except FileNotFoundError:
return None

def __getitem__(self, key: str) -> str | None:
return self.get(key)

def write(self, key: str, value: Any) -> None:
"""Set a value in the DB."""
with (self.path / key).open(mode="wb") as f:
f.write(value)

def delete(self, key: str) -> None:
"""Delete a value from the DB."""
with contextlib.suppress(FileNotFoundError):
Path.unlink(self.path / key)
18 changes: 18 additions & 0 deletions slack_bot/functions/create_issue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "create_issue",
"description": "Create a YouTrack issue",
"parameters": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "Issue summary (come up with an issue title here)"
},
"description": {
"type": "string",
"description": "Issue description (pass filled ${template} here)"
}
},
"required": ["summary", "description"]
}
}
13 changes: 13 additions & 0 deletions slack_bot/functions/run_query
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "run_query",
"description": "Runs SQL query against a db and returns result",
"parameters": {
"type": "object",
"properties": {
"sql_query": {
"type": "string",
"description": "SQL query to run"
}
}
}
}
192 changes: 96 additions & 96 deletions slack_bot/poetry.lock

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions slack_bot/prompts/clarification
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Act as a ClarifAI🧙🏾‍♂️, an expert at technical specifications clarification.
You are going to assist user with ${technical specification}, which will be assigned to Analytics team.
The ${user} is not an analyst, so you are helping ${user} with adjustment of ${technical specification} to map their ${goal}.

Your initialization: ${emoji}: My name is ${name}.
My task ends when technical specification will be adjusted.
${first step, question}."

Follow these steps:
1. ${Initialization}; Ask ${user} to describe the ${goal} in a few words.
2. Learn ${Product knowledge}.
3. Fill ${template}, based on the user's answers:
{template}
4. 🧙🏾‍♂️ make sure that ${user} is agreed with the final technical specification.
5. When the ${final technical specification} is confirmed, end the task with function call: `create_issue`, fill `description` with the final technical specification.

Rules:
-End every output with a multiple choice question with several options (not more than 4) to chose from
-Don't ask a colleague about data structure, data sources or any information about the tables and fields available in database
-Don't work with SQL, work only with ${technical specification} as an YouTrack issue

Product knowledge:
At our product users can make submissions. Submission is a user answer to a step.
Step is a educational unit that contains a question or some task to do.
12 changes: 12 additions & 0 deletions slack_bot/prompts/developing
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Act as a QueryMaster🧙🏾‍♂️, an expert SQL developer, who is using "ClickHouse" dialect.
Your job is to map technical specification to SQL query.
Develop a SQL query.

Rules:
-There are several available tables for you to work with:
`hyperskill.content` – events mart, contains information about user actions
`hyperskill_private.users` – users mart, contains user properties
-Use CTEs (common table expressions) to break down complex queries into smaller steps
-Don't run queries against `hyperskill.content` without `date` column filter or it will take consume all server resources
-Don't use `CASE`, use if() or multiIf() instead
-Don't use `COUNT(DISTINCT {column})` use `uniqExact({column})` instead
14 changes: 14 additions & 0 deletions slack_bot/prompts/reviewing
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Act as a QueryReviewer🧙🏾‍♂️, an expert SQL developer, who is reviewing quires in "ClickHouse" dialect.
Your job is to review a SQL query:
{sql_query}

Follow the next steps:
1. Check if {query} makes sense in context of the technical specification:
{tech_spec}
2. Check if {query} is valid according to docs:
{docs}
3. Check some other aspects of the {query}
4. Provide a step by step review of the {query} as a list of comments

Useful reminders:
-Don't run queries against `hyperskill.content` without `date` column filter or it will take consume all server resources
5 changes: 5 additions & 0 deletions slack_bot/prompts/testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Act as a QueryTester🧙🏾‍♂️, an expert SQL developer, who is testing queries in ClickHouse dialect.
Your job is to test this SQL query:
{sql_query}

Run {query} against a db, if errors occur, you will be informed, so you can fix them.
2 changes: 0 additions & 2 deletions slack_bot/run-tasks.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/bin/bash

cd slack_bot || exit

echo "Formatting code..."
black .

Expand Down
10 changes: 10 additions & 0 deletions slack_bot/templates/yt_issue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Motivation (Why?)
[comment]: <> (Please, describe the motivation for this task. Why do you need it? What is the problem you are trying to solve?)

### TODO:
[comment]: <> (What do you expect to see as a result of this task? What should the results of this task contain?)

### Priority & Urgency
[comment]: <> (Please, fill the following checklist:)
- is important [comment]: <> (Ask user about issue priority: Critical, Major, Normal, Minor.)
- is urgent [comment]: <> (Ask user about issue urgency: yes, no, may be there is a due date.)
85 changes: 73 additions & 12 deletions slack_bot/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
from __future__ import annotations

import json
import os
import re
import traceback
from pathlib import Path
from typing import Any, TYPE_CHECKING

import tiktoken
from dotenv import load_dotenv
from trafilatura import extract, fetch_url
from trafilatura.settings import use_config

from slack_bot.db import DB
from youtrack import YouTrack

load_dotenv()


Expand All @@ -22,14 +28,17 @@
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
SLACK_APP_TOKEN = os.environ.get("SLACK_APP_TOKEN")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")

SYSTEM_PROMPT = """
You are an AI assistant.
You will answer the question as truthfully as possible.
If you're unsure of the answer, say Sorry, I don't know.
"""
YT_BASE_URL = os.environ.get("YT_BASE_URL")
YT_API_TOKEN = os.environ.get("YT_API_TOKEN")

prompts = DB(Path(__file__).parent / "prompts")
templates = DB(Path(__file__).parent / "templates")
functions = DB(Path(__file__).parent / "functions")
AN_COMMAND = "an"
YT_COMMAND = "yt"
WAIT_MESSAGE = "Got your request. Please wait."
MAX_TOKENS = 8192
MODEL = "gpt-4"


def extract_url_list(text: str) -> list[str] | None:
Expand Down Expand Up @@ -76,7 +85,7 @@ def num_tokens_from_messages(

elif model == "gpt-4": # noqa: RET505
print( # noqa: T201
"Warning: gpt-4 may change over time."
"Warning: gpt-4 may change over time. "
"Returning num tokens assuming gpt-4-0314."
)

Expand Down Expand Up @@ -136,7 +145,18 @@ def process_message(message: dict[str, str], bot_user_id: str, role: str) -> str
def process_conversation_history(
conversation_history: SlackResponse, bot_user_id: str
) -> list[dict[str, str]]:
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages = []

cleaned_message = (
conversation_history["messages"][0]["text"]
.replace(f"<@{bot_user_id}>", "")
.strip()
)

if cleaned_message == AN_COMMAND:
conversation_history["messages"].pop(0)
system = prompts["clarification"].replace("{template}", templates["yt_issue"])
messages.append({"role": "system", "content": system})

for message in conversation_history["messages"][:-1]:
role = "assistant" if message["user"] == bot_user_id else "user"
Expand All @@ -156,6 +176,38 @@ def get_conversation_history(
)


def submit_issue(messages: list[dict[str, str]], openai: Any) -> str:
funcs = [json.loads(functions["create_issue"])]
openai_response = openai.ChatCompletion.create(
model=MODEL,
messages=messages,
functions=funcs,
function_call={"name": "create_issue"},
)

arguments = json.loads(
openai_response.choices[0]
.message.get("function_call", {})
.get("arguments", {}),
strict=False,
)

yt = YouTrack(
base_url=YT_BASE_URL,
token=YT_API_TOKEN,
)

response_text = yt.create_issue(
summary=arguments["summary"],
description=arguments["description"],
)

if isinstance(response_text, Exception):
raise response_text

return f"{YT_BASE_URL}/issue/{response_text['id']}"


def make_ai_response(
app: App, body: dict[str, dict[str, str]], context: dict[str, str], openai: Any
) -> None:
Expand All @@ -171,13 +223,21 @@ def make_ai_response(

conversation_history = get_conversation_history(app, channel_id, thread_ts)
messages = process_conversation_history(conversation_history, bot_user_id)

num_tokens = num_tokens_from_messages(messages)
print(f"Number of tokens: {num_tokens}") # noqa: T201

openai_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo", messages=messages
)
response_text = openai_response.choices[0].message["content"]
last_msg = messages[-1]

if (last_msg["role"] == "user") & (last_msg["content"] == YT_COMMAND):
messages.pop()
response_text = submit_issue(messages=messages, openai=openai)
else:
openai_response = openai.ChatCompletion.create(
model=MODEL, messages=messages
)
response_text = openai_response.choices[0].message["content"]

app.client.chat_update(
channel=channel_id, ts=reply_message_ts, text=response_text
)
Expand All @@ -188,3 +248,4 @@ def make_ai_response(
thread_ts=thread_ts,
text=f"I can't provide a response. Encountered an error:\n`\n{e}\n`",
)
traceback.print_exc()
48 changes: 48 additions & 0 deletions slack_bot/youtrack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from typing import Any

import requests


class IssueCreationError(Exception):
"""Exception raised for errors in the issue creation process."""

def __init__(self, status_code: int, error_message: str) -> None:
self.status_code = status_code
self.error_message = error_message
super().__init__(
f"Failed to create issue. Status code: {status_code}.\n"
f"Error: {error_message}"
)


class YouTrack:
def __init__(self, base_url: str | None, token: str | None) -> None:
if base_url or token:
self.base_url = base_url
self.token = token
else:
raise ValueError("YouTrack base URL and API token are required.")

def create_issue(
self, summary: str, description: str, project: str = "43-46"
) -> dict[str, Any] | IssueCreationError:
"""Create an issue in YouTrack."""
url = f"{self.base_url}/api/issues"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.token}",
}
payload = {
"project": {"id": project},
"summary": summary + " [created by AI Data Assistant]",
"description": description,
}

response = requests.post(url, headers=headers, json=payload, timeout=30)

if response.status_code != 200: # noqa: PLR2004
return IssueCreationError(response.status_code, response.text)

return response.json()

0 comments on commit 2c24502

Please sign in to comment.