Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fbfe676
Hackbot init
ReehalS Jan 4, 2026
28de7b2
Lint fixes
ReehalS Jan 30, 2026
b3627aa
Update to google also
ReehalS Jan 30, 2026
abcbcf8
Update hackbotSeed.mjs
ReehalS Feb 27, 2026
dd74403
Merge branch 'main' into hackbot
ReehalS Feb 27, 2026
c2a0545
Update hackbot knowledge to have admin UI and cleanup providers
ReehalS Feb 27, 2026
8bd3f9a
Make event fetching into a tool call rather than embedded into knowledge
ReehalS Feb 27, 2026
bfc11fd
Import knowledge via a doc
ReehalS Feb 27, 2026
8b48466
Add page aware bot
ReehalS Feb 27, 2026
21d31eb
Add ID to each section
ReehalS Feb 27, 2026
3a5e740
Add events, user info, and event cards
ReehalS Feb 27, 2026
fe2c60c
Hackbot UI
ReehalS Feb 27, 2026
00db8da
Decompose Hub Admin page
ReehalS Feb 27, 2026
8d4b8c3
Improve Hackbot functionality around meals, events, user preferences.
ReehalS Feb 27, 2026
9b6b4c5
Move Hackbot types
ReehalS Feb 27, 2026
342a19f
Decompose functions further
ReehalS Feb 27, 2026
5ed7021
Remove hackbot history on logout
ReehalS Feb 27, 2026
ab1d62d
Lint fixes
ReehalS Feb 27, 2026
23a2714
Merge branch 'main' into hackbot
ReehalS Feb 28, 2026
8c88210
Hackbot Vector search Fallback, prompt improvement, events sorting
ReehalS Feb 28, 2026
bf64f9a
Add starterkit info and scroll to page for starterkit
ReehalS Feb 28, 2026
6a1c433
Hackbot Improvements
ReehalS Mar 1, 2026
5096437
Delete old knowledge and improve current base set.
ReehalS Mar 1, 2026
7bbe5a5
Update vercel ai sdk version to v6 and model to gpt-5-mini
ReehalS Mar 1, 2026
924e6e7
Improve prompt caching, link+event filtering, usage metrics
ReehalS Mar 1, 2026
46f51d8
lint fixes
ReehalS Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .github/workflows/production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install node
uses: actions/setup-node@v4
with:
Expand All @@ -24,12 +24,19 @@ jobs:

- name: Install dependencies
run: npm install

- name: Run migrations
run: npx migrate-mongo up
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}

- name: Seed hackbot documentation
run: npm run hackbot:seed
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL }}

- name: Install Vercel CLI
run: npm install --global vercel@latest

Expand All @@ -40,11 +47,14 @@ jobs:
printf "${{ secrets.HMAC_INVITE_SECRET }}" | vercel env add HMAC_INVITE_SECRET production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ secrets.SENDER_PWD }}" | vercel env add SENDER_PWD production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ secrets.CHECK_IN_CODE }}" | vercel env add CHECK_IN_CODE production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ secrets.OPENAI_API_KEY }}" | vercel env add OPENAI_API_KEY production --force --token=${{ secrets.VERCEL_TOKEN }}

printf "${{ vars.ENV_URL }}" | vercel env add BASE_URL production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.INVITE_DEADLINE }}" | vercel env add INVITE_DEADLINE production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.SENDER_EMAIL }}" | vercel env add SENDER_EMAIL production --force --token=${{ secrets.VERCEL_TOKEN }}

printf "${{ vars.OPENAI_MODEL }}" | vercel env add OPENAI_MODEL production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.OPENAI_EMBEDDING_MODEL }}" | vercel env add OPENAI_EMBEDDING_MODEL production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.OPENAI_MAX_TOKENS }}" | vercel env add OPENAI_MAX_TOKENS production --force --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
Expand All @@ -56,4 +66,4 @@ jobs:
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

- name: Success
run: echo "🚀 Deploy successful - BLAST OFF WOO! (woot woot) !!! 🐕 🐕 🐕 🚀 "
run: echo "🚀 Deploy successful - BLAST OFF WOO! (woot woot) !!! 🐕 🐕 🐕 🚀 "
15 changes: 13 additions & 2 deletions .github/workflows/staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install node
uses: actions/setup-node@v4
with:
Expand All @@ -26,12 +26,19 @@ jobs:

- name: Install dependencies
run: npm install

- name: Run migrations
run: npx migrate-mongo up
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}

- name: Seed hackbot documentation
run: npm run hackbot:seed
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL }}

- name: Install Vercel CLI
run: npm install --global vercel@latest

Expand All @@ -42,10 +49,14 @@ jobs:
printf "${{ secrets.HMAC_INVITE_SECRET }}" | vercel env add HMAC_INVITE_SECRET production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ secrets.SENDER_PWD }}" | vercel env add SENDER_PWD production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ secrets.CHECK_IN_CODE }}" | vercel env add CHECK_IN_CODE production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ secrets.OPENAI_API_KEY }}" | vercel env add OPENAI_API_KEY production --force --token=${{ secrets.VERCEL_TOKEN }}

printf "${{ vars.ENV_URL }}" | vercel env add BASE_URL production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.INVITE_DEADLINE }}" | vercel env add INVITE_DEADLINE production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.SENDER_EMAIL }}" | vercel env add SENDER_EMAIL production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.OPENAI_MODEL }}" | vercel env add OPENAI_MODEL production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.OPENAI_EMBEDDING_MODEL }}" | vercel env add OPENAI_EMBEDDING_MODEL production --force --token=${{ secrets.VERCEL_TOKEN }}
printf "${{ vars.OPENAI_MAX_TOKENS }}" | vercel env add OPENAI_MAX_TOKENS production --force --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
Expand Down
270 changes: 270 additions & 0 deletions app/(api)/_actions/hackbot/askHackbot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { retrieveContext } from '@datalib/hackbot/getHackbotContext';
import { generateText, tool, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { getDatabase } from '@utils/mongodb/mongoClient.mjs';
import { retryWithBackoff } from '@utils/hackbot/retryWithBackoff';
import { formatEventDateTime } from '@utils/hackbot/eventFormatting';
import type {
HackbotMessage,
HackbotMessageRole,
HackbotResponse,
} from '@typeDefs/hackbot';

export type { HackbotMessage, HackbotMessageRole, HackbotResponse };

const MAX_USER_MESSAGE_CHARS = 200;
const MAX_HISTORY_MESSAGES = 6;
const MAX_ANSWER_WORDS = 180;

function truncateToWords(text: string, maxWords: number): string {
const words = text.trim().split(/\s+/);
if (words.length <= maxWords) return text.trim();
return words.slice(0, maxWords).join(' ') + '...';
}

const getEventsTool = tool({
description:
'Fetch the live HackDavis event schedule from the database. Use this for ANY question about event times, locations, schedule, or what is happening when.',
inputSchema: z.object({
type: z
.string()
.nullable()
.describe(
'Optional event type filter (e.g. "workshop", "meal", "ceremony"). Pass null to include all types.'
),
}),
execute: async ({ type }) => {
try {
const db = await getDatabase();
const query = type ? { type: { $regex: type, $options: 'i' } } : {};
const events = await db
.collection('events')
.find(query)
.sort({ start_time: 1 })
.toArray();

if (!events.length) {
return { events: [], message: 'No events found.' };
}

const formatted = events.map((ev: any) => ({
name: String(ev.name || 'Event'),
type: ev.type || null,
start: formatEventDateTime(ev.start_time),
end: formatEventDateTime(ev.end_time),
location: ev.location || null,
host: ev.host || null,
tags: Array.isArray(ev.tags) ? ev.tags : [],
}));

console.log(
`[hackbot][askHackbot][tool] get_events returned ${formatted.length} events`
);
return { events: formatted };
} catch (e) {
console.error('[hackbot][askHackbot][tool] get_events error', e);
return {
events: [],
message: 'Could not fetch events. Please check the schedule page.',
};
}
},
});

export async function askHackbot(
messages: HackbotMessage[]
): Promise<HackbotResponse> {
if (!messages.length) {
return { ok: false, answer: '', error: 'No messages provided.' };
}

const last = messages[messages.length - 1];

if (last.role !== 'user') {
return { ok: false, answer: '', error: 'Last message must be from user.' };
}

if (last.content.length > MAX_USER_MESSAGE_CHARS) {
return {
ok: false,
answer: '',
error: `Message too long. Please keep it under ${MAX_USER_MESSAGE_CHARS} characters.`,
};
}

const trimmedHistory = messages.slice(-MAX_HISTORY_MESSAGES);

let docs;
try {
({ docs } = await retrieveContext(last.content));
} catch (e) {
console.error('Hackbot context retrieval error', e);
return {
ok: false,
answer: '',
error:
'HackDavis Helper search backend is not configured (vector search unavailable). Please contact an organizer.',
};
}

const contextSummary =
docs && docs.length > 0
? docs
.map((d, index) => {
const header = `${index + 1}) [type=${d.type}, title="${d.title}"${
d.url ? `, url="${d.url}"` : ''
}]`;
return `${header}\n${d.text}`;
})
.join('\n\n')
: 'No additional knowledge context found.';

const primaryUrl = docs?.find((d) => d.url)?.url;

const systemPrompt =
'You are HackDavis Helper ("Hacky"), an AI assistant for the HackDavis hackathon. ' +
'CRITICAL: Your response MUST be under 200 tokens (~150 words). Be extremely concise. ' +
'CRITICAL: Only answer questions about HackDavis. Refuse unrelated topics politely. ' +
'CRITICAL: Only use facts from the provided context or tool results. Never invent times, dates, or locations. ' +
'You are friendly, helpful, and conversational. Use contractions ("you\'re", "it\'s") and avoid robotic phrasing. ' +
'For simple greetings ("hi", "hello"), respond warmly: "Hi, I\'m Hacky! I can help with questions about HackDavis." Keep it brief (1 sentence). ' +
'For event/schedule questions (times, locations, when something starts/ends): ' +
' - ALWAYS call the get_events tool to get the latest schedule. Do not guess or use cached knowledge. ' +
'For questions about HackDavis rules, submission, judging, tracks, or general info: ' +
' - Use the knowledge context below. ' +
'When answering: ' +
'1. Answer directly in 2-3 sentences using only facts from context or tool results. ' +
'2. For time/location questions: provide the full range if both start and end times exist. ' +
'3. For schedule questions: format as a bullet list, ordered chronologically. ' +
'Do NOT: ' +
'- Invent times, dates, locations, or URLs. ' +
'- Include URLs in your answer text (UI shows a separate "More info" link). ' +
'- Answer coding, homework, or general knowledge questions. ' +
'- Say "based on the context" or "according to the documents" (just answer directly). ' +
'If you cannot find an answer, say: "I don\'t have that information. Please ask an organizer or check the HackDavis website." ' +
'For unrelated questions, say: "Sorry, I can only answer questions about HackDavis. Do you have any questions about the event?"';

const fewShotExamples = [
{
role: 'user' as const,
content: 'When does hacking end?',
},
{
role: 'assistant' as const,
content: 'Hacking ends on Sunday, April 20 at 11:00 AM Pacific Time.',
},
{
role: 'user' as const,
content: 'What workshops are available?',
},
{
role: 'assistant' as const,
content:
'We have several workshops including:\n• Hackathons 101 (Sat 11:40 AM)\n• Getting Started with Git & GitHub (Sat 1:10 PM)\n• Intro to UI/UX (Sat 4:10 PM)\n• Hacking with LLMs (Sat 2:10 PM)',
},
];

const chatMessages = [
{ role: 'system', content: systemPrompt },
...fewShotExamples,
{
role: 'system',
content: `Knowledge context about HackDavis (rules, submission, judging, tracks, general info):\n\n${contextSummary}`,
},
...trimmedHistory.map((m) => ({
role: m.role,
content: m.content,
})),
];

try {
const model = process.env.OPENAI_MODEL || 'gpt-5-mini';
const maxOutputTokens = parseInt(
process.env.OPENAI_MAX_TOKENS || '600',
10
);

const startedAt = Date.now();
const { text, usage } = await retryWithBackoff(
() =>
generateText({
model: openai(model) as any,
messages: chatMessages.map((m) => ({
role: m.role as 'system' | 'user' | 'assistant',
content: m.content,
})),
maxOutputTokens,
stopWhen: stepCountIs(5),
tools: { get_events: getEventsTool },
providerOptions: {
openai: { reasoningEffort: 'low' },
},
}),
{
maxAttempts: 2,
delayMs: 2000,
backoffMultiplier: 2,
}
);

console.log('[hackbot][openai][chat]', {
model,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
ms: Date.now() - startedAt,
});

const answer = truncateToWords(text, MAX_ANSWER_WORDS);

return {
ok: true,
answer,
url: primaryUrl,
usage: {
chat: {
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
},
},
};
} catch (e: any) {
console.error('[hackbot][openai] Error', e);

if (e.status === 429) {
return {
ok: false,
answer: '',
error: 'Too many requests. Please wait a moment and try again.',
};
}

if (
e.message?.includes('ECONNREFUSED') ||
e.message?.includes('ETIMEDOUT')
) {
return {
ok: false,
answer: '',
error:
'Cannot reach AI service. Please check your connection or try again later.',
};
}

if (e.status === 401 || e.message?.includes('API key')) {
return {
ok: false,
answer: '',
error: 'AI service configuration error. Please contact an organizer.',
};
}

return {
ok: false,
answer: '',
error: 'Something went wrong. Please try again in a moment.',
};
}
}
Loading