diff --git a/docs/deployment.md b/docs/deployment.md index 6b09b6a..b2d391d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -98,6 +98,11 @@ Note: These steps have to be done by a Workspace Admin (otherwise it will be una LOG_LEVEL_STDERR="WARNING" # Override the log level for OpenTelemetry output LOG_LEVEL_OTEL="WARNING" + + # Optional: Enable stale ticket auto-close + # Tickets inactive for this many days will be automatically closed + # Leave unset to disable + STALE_TICKET_DAYS="7" ``` 4. Don't forget to click **Save All Environment Variables** diff --git a/nephthys/__main__.py b/nephthys/__main__.py index 268b533..e12e439 100644 --- a/nephthys/__main__.py +++ b/nephthys/__main__.py @@ -51,16 +51,23 @@ async def main(_app: Starlette): "cron", hour=14, minute=0, + day_of_week="mon-fri", timezone="Europe/London", ) - # scheduler.add_job( - # close_stale_tickets, - # "interval", - # hours=1, - # max_instances=1, - # next_run_time=datetime.now(), - # ) + from nephthys.tasks.close_stale import close_stale_tickets + from datetime import datetime + + if env.stale_ticket_days: + scheduler.add_job( + close_stale_tickets, + "interval", + hours=1, + max_instances=1, + next_run_time=datetime.now(), + ) + else: + logging.debug("Stale ticket closing has not been configured") scheduler.start() delete_msg_task = asyncio.create_task(process_queue()) diff --git a/nephthys/events/message_creation.py b/nephthys/events/message_creation.py index 858fc4c..f55faa0 100644 --- a/nephthys/events/message_creation.py +++ b/nephthys/events/message_creation.py @@ -350,7 +350,7 @@ async def generate_ticket_title(text: str): if not env.ai_client: return "No title available from AI." - model = "qwen/qwen3-32b" + model = "openai/gpt-oss-120b" try: response = await env.ai_client.chat.completions.create( model=model, @@ -358,8 +358,11 @@ async def generate_ticket_title(text: str): { "role": "system", "content": ( - "You are a helpful assistant that helps organise tickets for Hack Club's support team. You're going to take in a message and give it a title. " - "You will return no other content. Do *NOT* use title case. Avoid quote marks. Even if it's silly please summarise it. Use no more than 7 words, but as few as possible." + "You are a helpful assistant that helps organise tickets for Hack Club's support team. You're going to take in a message and give it a title." + "You will return no other content. Do NOT use title case but use capital letter at start of sentence + use capital letters for terms/proper nouns." + "Avoid quote marks. Even if it's silly please summarise it. Use no more than 7 words, but as few as possible" + "When mentioning Flavortown, do *NOT* change it to 'flavor town' or 'flavour town'. Hack Club should *NOT* be changed to 'hackclub'." + "Hackatime, Flavortown, and Hack Club should always be capitalized correctly. Same goes for terms like VSCode, PyCharm, API, and GitHub." ), }, { diff --git a/nephthys/tasks/close_stale.py b/nephthys/tasks/close_stale.py index 1d1369f..3e993bf 100644 --- a/nephthys/tasks/close_stale.py +++ b/nephthys/tasks/close_stale.py @@ -12,7 +12,7 @@ from nephthys.utils.logging import send_heartbeat from prisma.enums import TicketStatus -async def get_is_stale(ts: str, max_retries: int = 3) -> bool: +async def get_is_stale(ts: str, stale_ticket_days: int, max_retries: int = 3) -> bool: for attempt in range(max_retries): try: replies = await env.slack_client.conversations_replies( @@ -28,7 +28,7 @@ async def get_is_stale(ts: str, max_retries: int = 3) -> bool: return ( datetime.now(tz=timezone.utc) - datetime.fromtimestamp(float(last_reply["ts"]), tz=timezone.utc) - ) > timedelta(days=3) + ) > timedelta(days=stale_ticket_days) except SlackApiError as e: if e.response["error"] == "ratelimited": retry_after = int(e.response.headers.get("Retry-After", 1)) @@ -82,12 +82,22 @@ async def get_is_stale(ts: str, max_retries: int = 3) -> bool: async def close_stale_tickets(): """ - Closes tickets that have been open for more than 3 days. - This task is intended to be run periodically. + Closes tickets that have been inactive for more than the configured number of days, + based on the timestamp of the last message in the ticket's Slack thread. + + Configure via the STALE_TICKET_DAYS environment variable. + This task is intended to be run periodically (e.g., hourly). """ - logging.info("Closing stale tickets...") - await send_heartbeat("Closing stale tickets...") + stale_ticket_days = env.stale_ticket_days + if not stale_ticket_days: + logging.warning("Skipping ticket auto-close (STALE_TICKET_DAYS not set)") + return + + logging.info(f"Closing stale tickets, threshold_days={stale_ticket_days}") + await send_heartbeat( + f"Closing stale tickets (threshold: {stale_ticket_days} days)..." + ) try: tickets = await env.db.ticket.find_many( @@ -101,13 +111,13 @@ async def close_stale_tickets(): for i in range(0, len(tickets), batch_size): batch = tickets[i : i + batch_size] logging.info( - f"Processing batch {i // batch_size + 1}/{(len(tickets) + batch_size - 1) // batch_size}" + f"Processing stale tickets batch={i // batch_size + 1} batches={(len(tickets) + batch_size - 1) // batch_size}" ) for ticket in batch: await asyncio.sleep(1.2) # Rate limiting delay - if await get_is_stale(ticket.msgTs): + if await get_is_stale(ticket.msgTs, stale_ticket_days): stale += 1 resolver_user = ( ticket.assignedTo if ticket.assignedTo else ticket.openedBy @@ -130,7 +140,7 @@ async def close_stale_tickets(): await send_heartbeat(f"Closed {stale} stale tickets.") - logging.info(f"Closed {stale} stale tickets.") + logging.info(f"Closed stale tickets. count={stale}") except Exception as e: logging.error(f"Error closing stale tickets: {e}") await send_heartbeat(f"Error closing stale tickets: {e}") diff --git a/nephthys/transcripts/__init__.py b/nephthys/transcripts/__init__.py index b47d7d0..665dfa3 100644 --- a/nephthys/transcripts/__init__.py +++ b/nephthys/transcripts/__init__.py @@ -6,6 +6,7 @@ from nephthys.transcripts.transcripts.construct import Construct from nephthys.transcripts.transcripts.fallout import Fallout from nephthys.transcripts.transcripts.flavortown import Flavortown from nephthys.transcripts.transcripts.hctg import Hctg +from nephthys.transcripts.transcripts.help import Help from nephthys.transcripts.transcripts.identity import Identity from nephthys.transcripts.transcripts.jumpstart import Jumpstart from nephthys.transcripts.transcripts.lynx import Lynx @@ -24,4 +25,5 @@ transcripts: List[Type[Transcript]] = [ Hctg, Fallout, Lynx, + Help, ] diff --git a/nephthys/transcripts/transcripts/help.py b/nephthys/transcripts/transcripts/help.py new file mode 100644 index 0000000..c83ab3b --- /dev/null +++ b/nephthys/transcripts/transcripts/help.py @@ -0,0 +1,24 @@ +from nephthys.transcripts.transcript import Transcript + + +class Help(Transcript): + """Transcript for help""" + + program_name: str = "help" + program_owner: str = "U07F6FMJ97U" + + help_channel: str = "C07TM4C0AQ5" # help + ticket_channel: str = "C0AS7CGTK8W" # help-ticket + team_channel: str = "C0APKHZG495" # owners-of-help-channel + + faq_link: str = "https://hackclub.enterprise.slack.com/docs/T0266FRGM/F0ARLNM3A1E" + + first_ticket_create: str = f""" +hi (user)! seems like it's your first time, welcome to the help channel! someone will be here soon to help answer your question! for now, feel free to look at the <{faq_link}|faq>, it answers some common questions and gives basic information. +if your question has been answered, please hit the button below to mark it as resolved ^-^ +""" + ticket_create: str = f"someone will be here soon to help answer your question! for now, feel free to look at the <{faq_link}|faq>, it answers some common questions and gives basic information. if your question has been answered, please hit the button below to mark it as resolved ^-^" + resolve_ticket_button: str = "i get it now" + ticket_resolve: str = f"oh, oh! it looks like this post has been marked as resolved by <@{{user_id}}>! if you have any more questions, please make a new post in <#{help_channel}> and someone'll be happy to help you out! not me though, i'm just a silly racoon ^-^" + + not_allowed_channel: str = f"heyo! doesn't seem like you're supposed to be in that channel, please reach out to <@{program_owner}> if that's wrong!" diff --git a/nephthys/utils/env.py b/nephthys/utils/env.py index c98b50c..0c6de39 100644 --- a/nephthys/utils/env.py +++ b/nephthys/utils/env.py @@ -63,6 +63,25 @@ class Environment: self.slack_heartbeat_channel = os.environ.get("SLACK_HEARTBEAT_CHANNEL") + # Stale ticket auto-close: number of days of inactivity before closing + # Set to a positive integer to enable, leave unset to disable + stale_days_str = os.environ.get("STALE_TICKET_DAYS") + if stale_days_str: + try: + stale_days = int(stale_days_str) + self.stale_ticket_days = stale_days if stale_days > 0 else None + if stale_days <= 0: + logging.warning( + f"STALE_TICKET_DAYS must be positive, got {stale_days}. Disabling." + ) + except ValueError: + logging.warning( + f"Invalid STALE_TICKET_DAYS value: {stale_days_str}. Disabling." + ) + self.stale_ticket_days = None + else: + self.stale_ticket_days = None + unset = [key for key, value in self.__dict__.items() if value == "unset"] if unset: diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 81d2f9a..4ba21b0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -146,3 +146,5 @@ model CategoryTag { createdAt DateTime @default(now()) } + +