Merge branch 'main' into category-tag-improvements

This commit is contained in:
Mish 2026-04-14 20:46:09 +01:00 committed by GitHub
commit 0c9101ec26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 91 additions and 19 deletions

View file

@ -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**

View file

@ -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())

View file

@ -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."
),
},
{

View file

@ -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}")

View file

@ -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,
]

View file

@ -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!"

View file

@ -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:

View file

@ -146,3 +146,5 @@ model CategoryTag {
createdAt DateTime @default(now())
}