From 367dd5a2a1a6094f6cfdd45f04e1bf2decf78c69 Mon Sep 17 00:00:00 2001 From: "Obay M. Rashad" <102649548+ObayM@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:30:22 +0200 Subject: [PATCH] AI-powered tickets categorization and a daily fulfillment reminder for open tickets (#153) * Add ai-based ticket categorization and you can add tags in the bot homepage * made the CategoryTag model and refactored the code to support it * Implemented the fulfillment reminder to send all open tickets in the past 24h for amber to check them :) * removed the ability to manage categories from the UI/fixed stuff * Fix type error * removed question tags * Re-add question tags to DB schema This is only because we don't have migrations rn so we can't actually delete stuff from the DB --------- Co-authored-by: MMK21Hub <50421330+MMK21Hub@users.noreply.github.com> --- .env.sample | 2 +- README.md | 9 +- nephthys/__main__.py | 10 ++ ...question_tag.py => assign_category_tag.py} | 12 +- nephthys/actions/create_question_tag.py | 49 -------- .../events/message/send_backend_message.py | 42 ++----- nephthys/events/message_creation.py | 119 +++++++++++++++--- nephthys/macros/__init__.py | 2 + nephthys/macros/reopen.py | 2 +- .../macros/trigger_fulfillment_reminder.py | 20 +++ .../{question_tags.py => category_tags.py} | 8 +- nephthys/tasks/fulfillment_reminder.py | 105 ++++++++++++++++ nephthys/utils/slack.py | 32 ++--- nephthys/views/home/components/header.py | 4 +- nephthys/views/modals/create_question_tag.py | 43 ------- prisma/schema.prisma | 15 +++ 16 files changed, 297 insertions(+), 177 deletions(-) rename nephthys/actions/{assign_question_tag.py => assign_category_tag.py} (88%) delete mode 100644 nephthys/actions/create_question_tag.py create mode 100644 nephthys/macros/trigger_fulfillment_reminder.py rename nephthys/options/{question_tags.py => category_tags.py} (71%) create mode 100644 nephthys/tasks/fulfillment_reminder.py delete mode 100644 nephthys/views/modals/create_question_tag.py diff --git a/.env.sample b/.env.sample index 9e4c81e..74b066a 100644 --- a/.env.sample +++ b/.env.sample @@ -11,6 +11,6 @@ SLACK_USER_GROUP="S..." SLACK_MAINTAINER_ID="U..." DATABASE_URL="postgresql://postgres:postgres@localhost:5432/nephthys" APP_TITLE="helper heidi" -PROGRAM="summer_of_making" +PROGRAM="flavortown" HACK_CLUB_AI_API_KEY="sk-hc-v1-..." BASE_URL="https://..." diff --git a/README.md b/README.md index 4c0deb9..0c8c960 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # Nephthys -Nephthys is the bot powering #summer-of-making-help and #identity-help in the Hack Club Slack! Below is a guide to set her up for developing and here's a list of some of her features :) +Nephthys is the bot powering many support channels in the Hack Club Slack such as #flavortown-help and #identity-help! Below is a guide to set her up for developing and here's a list of some of her features :) ## Features -### Question tags -Quite often in help channels, the same question gets asked again and again. Helpers can associate tickets with a "question tag", which are pre-defined questions/issue such as "What's the payout range?", "How to top up grant?", or "404 on /shop". We can then keep track of how these questions trend over time, to provide a feedback loop for people building the event/platform/YSWS. +### Category tags -They can be added to tickets in the private tickets channel. +Category tags are used to classify tickets into broader categories such as "Fulfillment", "Identity", or "Platform Issues". When a new ticket is created, AI analyzes the message content and automatically assigns the most relevant category tag. + +Helpers can reassign these tags in the private tickets channel if the AI suggestion is incorrect. ### Team tags diff --git a/nephthys/__main__.py b/nephthys/__main__.py index 1ff30ca..138f56c 100644 --- a/nephthys/__main__.py +++ b/nephthys/__main__.py @@ -11,6 +11,7 @@ from starlette.applications import Starlette from nephthys.tasks.close_stale import close_stale_tickets from nephthys.tasks.daily_stats import send_daily_stats +from nephthys.tasks.fulfillment_reminder import send_fulfillment_reminder from nephthys.tasks.update_helpers import update_helpers from nephthys.utils.delete_thread import process_queue from nephthys.utils.env import env @@ -46,6 +47,15 @@ async def main(_app: Starlette): scheduler = AsyncIOScheduler(timezone="Europe/London") if env.daily_summary: scheduler.add_job(send_daily_stats, "cron", hour=0, minute=0) + + scheduler.add_job( + send_fulfillment_reminder, + "cron", + hour=14, + minute=0, + timezone="Europe/London", + ) + scheduler.add_job( close_stale_tickets, "interval", diff --git a/nephthys/actions/assign_question_tag.py b/nephthys/actions/assign_category_tag.py similarity index 88% rename from nephthys/actions/assign_question_tag.py rename to nephthys/actions/assign_category_tag.py index bdf1e12..90a74cd 100644 --- a/nephthys/actions/assign_question_tag.py +++ b/nephthys/actions/assign_category_tag.py @@ -10,7 +10,7 @@ from nephthys.events.message.send_backend_message import backend_message_fallbac from nephthys.utils.env import env -async def assign_question_tag_callback( +async def assign_category_tag_callback( ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient ): await ack() @@ -30,7 +30,7 @@ async def assign_question_tag_callback( user = await env.db.user.find_unique(where={"slackId": user_id}) if not user or not user.helper: logging.warning( - f"Unauthorized user attempted to assign question tag user_id={user_id}" + f"Unauthorized user attempted to assign category tag user_id={user_id}" ) await client.chat_postEphemeral( channel=channel_id, @@ -42,7 +42,7 @@ async def assign_question_tag_callback( ticket = await env.db.ticket.update( where={"ticketTs": ts}, data={ - "questionTag": ( + "categoryTag": ( {"connect": {"id": tag_id}} if tag_id is not None else {"disconnect": True} @@ -52,7 +52,7 @@ async def assign_question_tag_callback( ) if not ticket: logging.error( - f"Failed to find corresponding ticket to update question tag ticket_ts={ts}" + f"Failed to find corresponding ticket to update category tag ticket_ts={ts}" ) return @@ -78,11 +78,11 @@ async def assign_question_tag_callback( author_user_id=ticket.openedBy.slackId, msg_ts=ticket.msgTs, past_tickets=other_tickets, - current_question_tag_id=tag_id, + current_category_tag_id=tag_id, reopened_by=ticket.reopenedBy, ), ) logging.info( - f"Updated question tag on ticket ticket_id={ticket.id} tag_id={tag_id}" + f"Updated category tag on ticket ticket_id={ticket.id} tag_id={tag_id}" ) diff --git a/nephthys/actions/create_question_tag.py b/nephthys/actions/create_question_tag.py deleted file mode 100644 index ed4b3ae..0000000 --- a/nephthys/actions/create_question_tag.py +++ /dev/null @@ -1,49 +0,0 @@ -from slack_bolt.async_app import AsyncAck -from slack_sdk.web.async_client import AsyncWebClient - -from nephthys.utils.env import env -from nephthys.utils.logging import send_heartbeat -from nephthys.views.modals.create_question_tag import get_create_question_tag_modal - - -async def create_question_tag_view_callback( - ack: AsyncAck, body: dict, client: AsyncWebClient -): - """ - Callback for the create question tag view submission - """ - await ack() - user_id = body["user"]["id"] - - user = await env.db.user.find_unique(where={"slackId": user_id}) - if not user or not user.helper: - await send_heartbeat( - f"Attempted to create question tag by non-helper <@{user_id}>" - ) - return - - label = body["view"]["state"]["values"]["tag_label"]["tag_label"]["value"] - await env.db.questiontag.create(data={"label": label}) - - # await open_app_home("question-tags", client, user_id) - - -async def create_question_tag_btn_callback( - ack: AsyncAck, body: dict, client: AsyncWebClient -): - """ - Open modal to create a question tag - """ - await ack() - user_id = body["user"]["id"] - trigger_id = body["trigger_id"] - - user = await env.db.user.find_unique(where={"slackId": user_id}) - if not user or not user.helper: - await send_heartbeat( - f"Attempted to open create-question-tag modal by non-helper <@{user_id}>" - ) - return - - view = get_create_question_tag_modal() - await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id) diff --git a/nephthys/events/message/send_backend_message.py b/nephthys/events/message/send_backend_message.py index 32f3a38..4defc19 100644 --- a/nephthys/events/message/send_backend_message.py +++ b/nephthys/events/message/send_backend_message.py @@ -8,62 +8,46 @@ async def backend_message_blocks( author_user_id: str, msg_ts: str, past_tickets: int, - current_question_tag_id: int | None = None, + current_category_tag_id: int | None = None, reopened_by: User | None = None, ) -> list[dict]: thread_url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{msg_ts.replace('.', '')}" - if current_question_tag_id is not None: + if current_category_tag_id is not None: options = [ { "text": { "type": "plain_text", - "text": tag.label, + "text": tag.name, }, "value": f"{tag.id}", } - for tag in await env.db.questiontag.find_many() + for tag in await env.db.categorytag.find_many() ] initial_option = [ option for option in options - if option["value"] == f"{current_question_tag_id}" + if option["value"] == f"{current_category_tag_id}" ][0] else: initial_option = None - question_tags_dropdown = { + category_tags_dropdown = { "type": "input", - "label": {"type": "plain_text", "text": "Question tag", "emoji": True}, + "label": {"type": "plain_text", "text": "Category tag", "emoji": True}, "element": { "type": "external_select", - "action_id": "question-tag-list", + "action_id": "category-tag-list", "placeholder": { "type": "plain_text", - "text": "Which question tag fits?", + "text": "Which category tag fits?", }, "min_query_length": 0, }, } if initial_option: - question_tags_dropdown["element"]["initial_option"] = initial_option + category_tags_dropdown["element"]["initial_option"] = initial_option return [ - question_tags_dropdown, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "If none of the existing tags fit :point_right:", - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": ":wrench: new question tag", - "emoji": True, - }, - "action_id": "create-question-tag", - }, - }, + category_tags_dropdown, { "type": "input", "label": {"type": "plain_text", "text": "Team tags", "emoji": True}, @@ -108,7 +92,7 @@ async def send_backend_message( description: str, past_tickets: int, client: AsyncWebClient, - current_question_tag_id: int | None = None, + current_category_tag_id: int | None = None, reopened_by: User | None = None, display_name: str | None = None, profile_pic: str | None = None, @@ -119,7 +103,7 @@ async def send_backend_message( channel=env.slack_ticket_channel, text=backend_message_fallback_text(author_user_id, description, reopened_by), blocks=await backend_message_blocks( - author_user_id, msg_ts, past_tickets, current_question_tag_id, reopened_by + author_user_id, msg_ts, past_tickets, current_category_tag_id, reopened_by ), username=display_name, icon_url=profile_pic, diff --git a/nephthys/events/message_creation.py b/nephthys/events/message_creation.py index edffa74..858fc4c 100644 --- a/nephthys/events/message_creation.py +++ b/nephthys/events/message_creation.py @@ -1,4 +1,5 @@ import logging +import string from datetime import datetime from typing import Any from typing import Dict @@ -8,6 +9,8 @@ from prometheus_client import Histogram from slack_sdk.errors import SlackApiError from slack_sdk.web.async_client import AsyncWebClient +from nephthys.events.message.send_backend_message import backend_message_blocks +from nephthys.events.message.send_backend_message import backend_message_fallback_text from nephthys.events.message.send_backend_message import send_backend_message from nephthys.macros import run_macro from nephthys.utils.env import env @@ -18,6 +21,7 @@ from nephthys.utils.ticket_methods import delete_and_clean_up_ticket from prisma.enums import TicketStatus from prisma.enums import UserType from prisma.models import User +from prisma.types import TicketCreateInput # Message subtypes that should be handled by on_message (messages with no subtype are always handled) ALLOWED_SUBTYPES = ["file_share", "me_message", "thread_broadcast"] @@ -28,6 +32,12 @@ TICKET_TITLE_GENERATION_DURATION = Histogram( ) +TICKET_CATEGORY_GENERATION_DURATION = Histogram( + "nephthys_ticket_category_generation_duration_seconds", + "How long it takes to generate a category tag using AI", +) + + async def handle_message_sent_to_channel(event: Dict[str, Any], client: AsyncWebClient): """Tell a non-helper off because they sent a thread message with the 'send to channel' box checked.""" await client.chat_delete( @@ -173,27 +183,55 @@ async def handle_new_question( ): title = await generate_ticket_title(text) + async with perf_timer( + "AI category tag generation", TICKET_CATEGORY_GENERATION_DURATION + ): + category_tag_id = await generate_category_tag(text) + + if category_tag_id: + blocks = await backend_message_blocks( + author_user_id=author_id, + msg_ts=event["ts"], + past_tickets=past_tickets, + current_category_tag_id=category_tag_id, + ) + + await client.chat_update( + channel=env.slack_ticket_channel, + ts=ticket_message_ts, + text=backend_message_fallback_text(author_id, text), + blocks=blocks, + ) + user_facing_message_ts = user_facing_message["ts"] if not user_facing_message_ts: logging.error(f"User-facing message has no ts: {user_facing_message}") return async with perf_timer("Creating ticket in DB"): - ticket = await env.db.ticket.create( - { - "title": title, - "description": text, - "msgTs": event["ts"], - "ticketTs": ticket_message_ts, - "openedBy": {"connect": {"id": db_user.id}}, - "userFacingMsgs": { - "create": { - "channelId": event["channel"], - "ts": user_facing_message_ts, - } - }, + ticket_data: TicketCreateInput = { + "title": title, + "description": text, + "msgTs": event["ts"], + "ticketTs": ticket_message_ts, + "openedBy": {"connect": {"id": db_user.id}}, + "userFacingMsgs": { + "create": { + "channelId": event["channel"], + "ts": user_facing_message_ts, + } }, - ) + } + + if category_tag_id: + ticket_data["categoryTag"] = {"connect": {"id": category_tag_id}} + + ticket = await env.db.ticket.create(ticket_data) + + if not category_tag_id: + logging.warning( + f"Failed to generate category tag for ticket_id={ticket.id}" + ) try: await client.reactions_add( @@ -341,3 +379,56 @@ async def generate_ticket_title(text: str): # Capitalise first letter title = title[0].upper() + title[1:] if len(title) > 1 else title.upper() return title + + +async def generate_category_tag(text: str) -> int | None: + category_tags = await env.db.categorytag.find_many() + + if not category_tags: + return None + + tag_options = ", ".join([tag.name for tag in category_tags]) + tag_map = {tag.name.lower(): tag for tag in category_tags} + + if not env.ai_client: + return None + + model = "google/gemini-3-flash-preview" + try: + response = await env.ai_client.chat.completions.create( + model=model, + messages=[ + { + "role": "system", + "content": ( + "You are a helpful assistant that categorizes support tickets! " + f"Choose the best tag from this list: [{tag_options}]. " + "Return ONLY the exact tag name. If none fit, return 'None'." + ), + }, + { + "role": "user", + "content": f"Ticket content: {text}", + }, + ], + ) + except OpenAIError as e: + await send_heartbeat(f"Failed to get AI response for tag generation: {e}") + return None + + if not (len(response.choices) and response.choices[0].message.content): + return None + + suggested_tag_label = response.choices[0].message.content.strip() + + suggested_clean = suggested_tag_label.strip(string.punctuation) + + original_label = tag_map.get(suggested_clean.lower()) + + if not original_label: + original_label = tag_map.get(suggested_tag_label.lower()) + + if original_label: + return original_label.id + + return None diff --git a/nephthys/macros/__init__.py b/nephthys/macros/__init__.py index bf76af5..c1385c2 100644 --- a/nephthys/macros/__init__.py +++ b/nephthys/macros/__init__.py @@ -10,6 +10,7 @@ from nephthys.macros.shipcertqueue import ShipCertQueue from nephthys.macros.shipwrights import Shipwrights from nephthys.macros.thread import Thread from nephthys.macros.trigger_daily_stats import DailyStats +from nephthys.macros.trigger_fulfillment_reminder import FulfillmentReminder from nephthys.macros.types import Macro from nephthys.utils.env import env from nephthys.utils.logging import send_heartbeat @@ -28,6 +29,7 @@ macro_list: list[type[Macro]] = [ Thread, Reopen, DailyStats, + FulfillmentReminder, Shipwrights, ] diff --git a/nephthys/macros/reopen.py b/nephthys/macros/reopen.py index 422298c..620a7ff 100644 --- a/nephthys/macros/reopen.py +++ b/nephthys/macros/reopen.py @@ -61,7 +61,7 @@ class Reopen(Macro): msg_ts=ticket.msgTs, past_tickets=other_tickets, client=env.slack_client, - current_question_tag_id=ticket.questionTagId, + current_category_tag_id=ticket.categoryTagId, reopened_by=helper, display_name=author.display_name(), profile_pic=author.profile_pic_512x(), diff --git a/nephthys/macros/trigger_fulfillment_reminder.py b/nephthys/macros/trigger_fulfillment_reminder.py new file mode 100644 index 0000000..b839689 --- /dev/null +++ b/nephthys/macros/trigger_fulfillment_reminder.py @@ -0,0 +1,20 @@ +from nephthys.macros.types import Macro +from nephthys.tasks import fulfillment_reminder +from nephthys.utils.env import env + + +class FulfillmentReminder(Macro): + name = "fulfillment_reminder" + can_run_on_closed = True + + async def run(self, ticket, helper, **kwargs): + """Development-only macro to manually trigger the fulfillment reminder message""" + if not env.environment == "development": + await env.slack_client.chat_postEphemeral( + channel=env.slack_help_channel, + thread_ts=ticket.msgTs, + user=helper.slackId, + text="The `fulfillment_reminder` macro can only be run in development environments.", + ) + return + await fulfillment_reminder.send_fulfillment_reminder() diff --git a/nephthys/options/question_tags.py b/nephthys/options/category_tags.py similarity index 71% rename from nephthys/options/question_tags.py rename to nephthys/options/category_tags.py index 102a86d..bbccd5a 100644 --- a/nephthys/options/question_tags.py +++ b/nephthys/options/category_tags.py @@ -6,14 +6,14 @@ from thefuzz import process from nephthys.utils.env import env -async def get_question_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]: - tags = await env.db.questiontag.find_many() +async def get_category_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]: + tags = await env.db.categorytag.find_many() if not tags: return [] keyword = payload.get("value") if keyword: - tag_names = [tag.label for tag in tags] + tag_names = [tag.name for tag in tags] scores = process.extract(keyword, tag_names, scorer=fuzz.ratio, limit=100) matching_tags = [tags[tag_names.index(score[0])] for score in scores] else: @@ -21,7 +21,7 @@ async def get_question_tags(payload: dict) -> list[dict[str, dict[str, str] | st res = [ { - "text": {"type": "plain_text", "text": f"{tag.label}"}, + "text": {"type": "plain_text", "text": f"{tag.name}"}, "value": str(tag.id), } for tag in matching_tags diff --git a/nephthys/tasks/fulfillment_reminder.py b/nephthys/tasks/fulfillment_reminder.py new file mode 100644 index 0000000..dfccaa6 --- /dev/null +++ b/nephthys/tasks/fulfillment_reminder.py @@ -0,0 +1,105 @@ +import logging +from datetime import datetime +from datetime import timedelta + +from nephthys.utils.env import env +from nephthys.utils.logging import send_heartbeat +from nephthys.utils.ticket_methods import get_question_message_link +from prisma.enums import TicketStatus + + +def slack_timestamp(dt: datetime, format: str = "date_short") -> str: + fallback = dt.isoformat().replace("T", " ") + return f"" + + +async def send_fulfillment_reminder(): + """ + Checks for 'Shop/fulfillment query' tag and sends a reminder with open tickets for the fulfillment team/amber + Run daily at 2 PM (London Time) + """ + + target_tag_name = "Shop/fulfillment query" + target_slack_id = "U054VC2KM9P" + + logging.info("Running fulfillment team reminder task") + + try: + tag = await env.db.categorytag.find_unique(where={"name": target_tag_name}) + + if not tag: + logging.info( + f"Tag '{target_tag_name}' not found. Skipping fulfillment reminder." + ) + return + + now = datetime.now() + twenty_four_hours_ago = now - timedelta(hours=24) + + tickets = await env.db.ticket.find_many( + where={ + "categoryTagId": tag.id, + "status": {"in": [TicketStatus.OPEN, TicketStatus.IN_PROGRESS]}, + "createdAt": {"gte": twenty_four_hours_ago}, + }, + include={"openedBy": True, "tagsOnTickets": {"include": {"tag": True}}}, + ) + + msg_header = f"oh hi <@{target_slack_id}>! i found some fulfillment tickets for you! :rac_cute:" + + if not tickets: + logging.info("No open fulfillment tickets found. Skipping Slack reminder.") + return + + else: + msg_lines = [ + f":rac_shy: *tickets needing attention ({len(tickets)})*", + "here are the open tickets from the last 24 hours:", + ] + + for i, ticket in enumerate(tickets): + label = ( + ticket.title or ticket.description[:100] or f"Ticket #{ticket.id}" + ) + + link = get_question_message_link(ticket) + created_ts = slack_timestamp(ticket.createdAt, format="date_short") + + tags = ticket.tagsOnTickets + tags_string = ( + " (" + ", ".join(f"*{t.tag.name}*" for t in tags if t.tag) + ")" + if tags + else "" + ) + + msg_lines.append( + f"{i + 1}. <{link}|{label}>{tags_string} (created {created_ts})" + ) + + msg_body = "\n".join(msg_lines) + + full_msg = f""" +{msg_header} + +{msg_body} +""" + + await env.slack_client.chat_postMessage( + channel=env.slack_bts_channel, + text=f"Reminder for <@{target_slack_id}>", + blocks=[{"type": "section", "text": {"type": "mrkdwn", "text": full_msg}}], + ) + + logging.info("Fulfillment reminder sent successfully.") + + except Exception as e: + logging.error(f"Failed to send fulfillment reminder: {e}", exc_info=True) + try: + await send_heartbeat( + "Failed to send fulfillment reminder", + messages=[str(e)], + ) + except Exception as slack_e: + logging.error( + f"Could not send error notification to Slack maintainer: {slack_e}" + ) diff --git a/nephthys/utils/slack.py b/nephthys/utils/slack.py index cd38db5..bf8b46e 100644 --- a/nephthys/utils/slack.py +++ b/nephthys/utils/slack.py @@ -6,10 +6,8 @@ from slack_bolt.async_app import AsyncApp from slack_bolt.context.ack.async_ack import AsyncAck from slack_sdk.web.async_client import AsyncWebClient -from nephthys.actions.assign_question_tag import assign_question_tag_callback +from nephthys.actions.assign_category_tag import assign_category_tag_callback from nephthys.actions.assign_team_tag import assign_team_tag_callback -from nephthys.actions.create_question_tag import create_question_tag_btn_callback -from nephthys.actions.create_question_tag import create_question_tag_view_callback from nephthys.actions.create_team_tag import create_team_tag_btn_callback from nephthys.actions.create_team_tag import create_team_tag_view_callback from nephthys.actions.resolve import resolve @@ -21,7 +19,7 @@ from nephthys.events.channel_join import channel_join from nephthys.events.channel_left import channel_left from nephthys.events.message_creation import on_message from nephthys.events.message_deletion import on_message_deletion -from nephthys.options.question_tags import get_question_tags +from nephthys.options.category_tags import get_category_tags from nephthys.options.team_tags import get_team_tags from nephthys.utils.env import env from nephthys.utils.performance import perf_timer @@ -62,9 +60,9 @@ async def handle_team_tag_list_options(ack: AsyncAck, payload: dict): await ack(options=tags) -@app.options("question-tag-list") -async def handle_question_tag_list_options(ack: AsyncAck, payload: dict): - tags = await get_question_tags(payload) +@app.options("category-tag-list") +async def handle_category_tag_list_options(ack: AsyncAck, payload: dict): + tags = await get_category_tags(payload) await ack(options=tags) @@ -100,13 +98,6 @@ async def create_team_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebC await create_team_tag_btn_callback(ack, body, client) -@app.action("create-question-tag") -async def create_question_tag( - ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient -): - await create_question_tag_btn_callback(ack, body, client) - - @app.view("create_team_tag") async def create_team_tag_view( ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient @@ -114,13 +105,6 @@ async def create_team_tag_view( await create_team_tag_view_callback(ack, body, client) -@app.view("create_question_tag") -async def create_question_tag_view( - ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient -): - await create_question_tag_view_callback(ack, body, client) - - @app.action("tag-subscribe") async def tag_subscribe(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient): await tag_subscribe_callback(ack, body, client) @@ -132,11 +116,11 @@ async def assign_team_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebC await assign_team_tag_callback(ack, body, client) -@app.action("question-tag-list") -async def assign_question_tag( +@app.action("category-tag-list") +async def assign_category_tag( ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient ): - await assign_question_tag_callback(ack, body, client) + await assign_category_tag_callback(ack, body, client) @app.command("/dm-magic-link") diff --git a/nephthys/views/home/components/header.py b/nephthys/views/home/components/header.py index c4cfd7f..dfcb138 100644 --- a/nephthys/views/home/components/header.py +++ b/nephthys/views/home/components/header.py @@ -2,7 +2,7 @@ from nephthys.utils.env import env from prisma.models import User -def header_buttons(current_view: str): +def header_buttons(current_view: str, user: User | None): buttons = [] buttons.append( @@ -59,6 +59,6 @@ def title_line(): def get_header(user: User | None, current: str = "dashboard") -> list[dict]: return [ title_line(), - header_buttons(current), + header_buttons(current, user), {"type": "divider"}, ] diff --git a/nephthys/views/modals/create_question_tag.py b/nephthys/views/modals/create_question_tag.py deleted file mode 100644 index d645435..0000000 --- a/nephthys/views/modals/create_question_tag.py +++ /dev/null @@ -1,43 +0,0 @@ -def get_create_question_tag_modal(): - return { - "type": "modal", - "callback_id": "create_question_tag", - "title": { - "type": "plain_text", - "text": "Create question tag", - "emoji": True, - }, - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": """ -Question tags are used to keep track of how often specific questions are being asked, so tag names should be brief but specific enough to uniquely identify the question. - -Examples: -• Missing CSS on site -• What is Flavortown? -• "You're not eligible" when trying to ship project - """, - }, - }, - { - "type": "input", - "block_id": "tag_label", - "label": { - "type": "plain_text", - "text": "Question label", - }, - "element": { - "type": "plain_text_input", - "action_id": "tag_label", - }, - }, - ], - "submit": { - "type": "plain_text", - "text": ":rac_question: Create!", - "emoji": True, - }, - } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5ee60fe..59af15f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,6 +33,7 @@ model User { reopenedTickets Ticket[] @relation("ReopenedTickets") tagSubscriptions UserTagSubscription[] + createdCategoryTags CategoryTag[] @relation("CreatedCategoryTags") helper Boolean @default(false) createdAt DateTime @default(now()) @@ -74,6 +75,9 @@ model Ticket { questionTag QuestionTag? @relation("QuestionTagTickets", fields: [questionTagId], references: [id]) questionTagId Int? + categoryTag CategoryTag? @relation("CategoryTagTickets", fields: [categoryTagId], references: [id]) + categoryTagId Int? + createdAt DateTime @default(now()) } @@ -128,3 +132,14 @@ model BotMessage { @@unique([ts, channelId]) } + +model CategoryTag { + id Int @id @unique @default(autoincrement()) + name String @unique + tickets Ticket[] @relation("CategoryTagTickets") + + createdBy User? @relation("CreatedCategoryTags", fields: [createdById], references: [id]) + createdById Int? + + createdAt DateTime @default(now()) +}