Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions app/(api)/_actions/hackbot/clearKnowledgeDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use server';

import { getDatabase } from '@utils/mongodb/mongoClient.mjs';

export interface ClearKnowledgeDocsResult {
ok: boolean;
deletedKnowledge: number;
deletedEmbeddings: number;
error?: string;
}

export default async function clearKnowledgeDocs(): Promise<ClearKnowledgeDocsResult> {
try {
const db = await getDatabase();

const knowledgeResult = await db
.collection('hackbot_knowledge')
.deleteMany({});

const embeddingsResult = await db
.collection('hackbot_docs')
.deleteMany({ _id: { $regex: '^knowledge-' } });

return {
ok: true,
deletedKnowledge: knowledgeResult.deletedCount,
deletedEmbeddings: embeddingsResult.deletedCount,
};
} catch (e) {
const msg = e instanceof Error ? e.message : 'Unknown error';
console.error('[clearKnowledgeDocs] Error:', msg);
return {
ok: false,
deletedKnowledge: 0,
deletedEmbeddings: 0,
error: msg,
};
}
}
30 changes: 30 additions & 0 deletions app/(api)/_actions/hackbot/deleteKnowledgeDoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use server';

import { getDatabase } from '@utils/mongodb/mongoClient.mjs';
import { ObjectId } from 'mongodb';

export interface DeleteKnowledgeDocResult {
ok: boolean;
error?: string;
}

export default async function deleteKnowledgeDoc(
id: string
): Promise<DeleteKnowledgeDocResult> {
try {
const db = await getDatabase();
const objectId = new ObjectId(id);

await db.collection('hackbot_knowledge').deleteOne({ _id: objectId });
await db.collection('hackbot_docs').deleteOne({ _id: `knowledge-${id}` });

console.log(`[deleteKnowledgeDoc] Deleted ${id}`);
return { ok: true };
} catch (e) {
console.error('[deleteKnowledgeDoc] Error', e);
return {
ok: false,
error: e instanceof Error ? e.message : 'Failed to delete document',
};
}
}
17 changes: 17 additions & 0 deletions app/(api)/_actions/hackbot/getHackerProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use server';

import { auth } from '@/auth';
import type { HackerProfile } from '@typeDefs/hackbot';

export type { HackerProfile };

export async function getHackerProfile(): Promise<HackerProfile | null> {
const session = await auth();
if (!session?.user) return null;
const user = session.user as any;
return {
name: user.name ?? undefined,
position: user.position ?? undefined,
is_beginner: user.is_beginner ?? undefined,
};
}
50 changes: 50 additions & 0 deletions app/(api)/_actions/hackbot/getKnowledgeDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use server';

import { getDatabase } from '@utils/mongodb/mongoClient.mjs';
import { HackDocType } from '@typeDefs/hackbot';

export interface KnowledgeDoc {
id: string;
type: HackDocType;
title: string;
content: string;
url: string | null;
createdAt: string;
updatedAt: string;
}

export interface GetKnowledgeDocsResult {
ok: boolean;
docs: KnowledgeDoc[];
error?: string;
}

export default async function getKnowledgeDocs(): Promise<GetKnowledgeDocsResult> {
try {
const db = await getDatabase();
const raw = await db
.collection('hackbot_knowledge')
.find({})
.sort({ updatedAt: -1 })
.toArray();

const docs: KnowledgeDoc[] = raw.map((d: any) => ({
id: String(d._id),
type: d.type,
title: d.title,
content: d.content,
url: d.url ?? null,
createdAt: d.createdAt?.toISOString?.() ?? new Date().toISOString(),
updatedAt: d.updatedAt?.toISOString?.() ?? new Date().toISOString(),
}));

return { ok: true, docs };
} catch (e) {
console.error('[getKnowledgeDocs] Error', e);
return {
ok: false,
docs: [],
error: e instanceof Error ? e.message : 'Failed to load knowledge docs',
};
}
}
59 changes: 59 additions & 0 deletions app/(api)/_actions/hackbot/getUsageMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use server';

import { getDatabase } from '@utils/mongodb/mongoClient.mjs';

export type UsagePeriod = '24h' | '7d' | '30d';

export interface UsageMetrics {
totalRequests: number;
totalPromptTokens: number;
totalCompletionTokens: number;
totalCachedTokens: number;
/** 0–1 fraction of prompt tokens that were served from cache */
cacheHitRate: number;
}

export async function getUsageMetrics(
period: UsagePeriod = '24h'
): Promise<UsageMetrics> {
const hours = period === '24h' ? 24 : period === '7d' ? 168 : 720;
const since = new Date(Date.now() - hours * 60 * 60 * 1000);

const db = await getDatabase();
const [result] = await db
.collection('hackbot_usage')
.aggregate([
{ $match: { timestamp: { $gte: since } } },
{
$group: {
_id: null,
totalRequests: { $sum: 1 },
totalPromptTokens: { $sum: '$promptTokens' },
totalCompletionTokens: { $sum: '$completionTokens' },
totalCachedTokens: { $sum: '$cachedPromptTokens' },
},
},
])
.toArray();

if (!result) {
return {
totalRequests: 0,
totalPromptTokens: 0,
totalCompletionTokens: 0,
totalCachedTokens: 0,
cacheHitRate: 0,
};
}

return {
totalRequests: result.totalRequests,
totalPromptTokens: result.totalPromptTokens,
totalCompletionTokens: result.totalCompletionTokens,
totalCachedTokens: result.totalCachedTokens,
cacheHitRate:
result.totalPromptTokens > 0
? result.totalCachedTokens / result.totalPromptTokens
: 0,
};
}
Loading
Loading