Skip to content

Commit 61c3ecc

Browse files
committed
Add basic chat bot
Add basic chat bot implementation including MOS lookup tool and required user auth
1 parent 1871c93 commit 61c3ecc

File tree

11 files changed

+4210
-3230
lines changed

11 files changed

+4210
-3230
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ public/workbox-*.js
5555

5656

5757
/src/generated/prisma
58+
.neon

package-lock.json

Lines changed: 3192 additions & 3005 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
},
2121
"dependencies": {
2222
"@ai-sdk/azure": "^1.0.7",
23+
"@ai-sdk/google": "^1.2.19",
24+
"@ai-sdk/react": "^1.2.12",
2325
"@googlemaps/react-wrapper": "^1.1.35",
2426
"@googlemaps/typescript-guards": "^2.0.1",
2527
"@neondatabase/serverless": "^1.0.1",
@@ -33,7 +35,7 @@
3335
"@types/express": "^4.17.17",
3436
"@vercel/analytics": "^1.4.1",
3537
"ace-builds": "^1.33.1",
36-
"ai": "^2.2.37",
38+
"ai": "^4.3.16",
3739
"axios": "^1.4.0",
3840
"class-variance-authority": "^0.7.0",
3941
"clsx": "^1.2.1",
@@ -64,7 +66,8 @@
6466
"swiper": "^8.3.1",
6567
"tailwind-merge": "^2.3.0",
6668
"tailwindcss-animate": "^1.0.7",
67-
"ws": "^8.18.2"
69+
"ws": "^8.18.2",
70+
"zod": "^3.25.67"
6871
},
6972
"devDependencies": {
7073
"@fullhuman/postcss-purgecss": "^4.1.3",
File renamed without changes.

src/app/api/chat/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { CoreMessage } from "ai";
2+
import { gemini } from "@/lib/ai/gemini";
3+
import { getServerSession } from "next-auth/next";
4+
import { NextRequest, NextResponse } from "next/server";
5+
import { options } from "@/pages/api/auth/options";
6+
7+
export async function POST(req: NextRequest) {
8+
const session = await getServerSession(options);
9+
if (!session) {
10+
return NextResponse.json({ error: "You must be logged in to access this API route." });
11+
}
12+
console.log(session);
13+
const { messages }: { messages: CoreMessage[] } = await req.json();
14+
const response = gemini.chat(messages);
15+
return response.toDataStreamResponse();
16+
}

src/lib/ai/chat-bot.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { type LanguageModel, streamText, type CoreMessage, generateObject } from "ai";
2+
import { type ZodType, type ZodTypeDef } from "zod";
3+
import { mosLookup } from "./tools/mos-lookup";
4+
5+
const baseContext = `
6+
You are a specialized AI assistant for Vets Who Code members, designed to provide clear, practical technical guidance to veterans transitioning into software development careers.\n\n
7+
CORE TECH STACK:\n
8+
- Frontend: JavaScript, TypeScript, React, Next.js\n
9+
- Styling: CSS, Tailwind CSS\n
10+
- Backend: Python, FastAPI\n
11+
- Data & Visualization: Streamlit\n
12+
- Advanced: AI/ML fundamentals\n
13+
- Development Tools: Git, GitHub, VS Code\n
14+
- Testing: Jest, Pytest\n\n
15+
16+
CAREER TRANSITION GUIDANCE:\n
17+
1. Resume Development:\n
18+
- Technical Skills: Programming Languages, Frameworks, Tools, Cloud, Testing\n
19+
- Military Experience Translation: Leadership, Problem-solving, Team Collaboration\n\n
20+
21+
2. Portfolio Development:\n
22+
- Clean code and documentation\n
23+
- Version control and API integration\n
24+
- Responsive design and performance\n
25+
- Testing and TypeScript implementation\n
26+
- Security and accessibility standards\n\n
27+
28+
LEARNING PATHS:\n
29+
1. Fundamentals: HTML, CSS, JavaScript, Git\n
30+
2. Intermediate: TypeScript, React, Python\n
31+
3. Advanced: Next.js, FastAPI, Streamlit, AI/ML\n\n
32+
33+
PROJECT FOCUS:\n
34+
1. Portfolio Projects: Personal website, APIs, Data visualization\n
35+
2. Technical Skills: Code quality, Testing, Security, Performance\n
36+
3. Career Materials: GitHub profile, Technical blog, Documentation\n\n
37+
38+
Remember: Provide practical guidance for building technical skills and transitioning to software development careers.
39+
Focus on concrete examples and best practices.
40+
`;
41+
42+
export class ChatBot {
43+
model: LanguageModel;
44+
45+
constructor(model: LanguageModel) {
46+
this.model = model;
47+
}
48+
49+
chat(messages: CoreMessage[], systemContext: string | null = baseContext) {
50+
const includedMessages: CoreMessage[] = [];
51+
if (systemContext) {
52+
includedMessages.push({
53+
role: "system",
54+
content: systemContext,
55+
});
56+
}
57+
includedMessages.push(...messages);
58+
59+
console.log("Sending messages to AI model:", includedMessages);
60+
const response = streamText({
61+
model: this.model,
62+
messages: includedMessages,
63+
tools: { mosLookup },
64+
onFinish: (message) => {
65+
// TODO - store response in database
66+
if (message.text) {
67+
console.error("Response received:", message.text);
68+
}
69+
70+
if (message.toolCalls && message.toolCalls.length > 0) {
71+
console.log("Tool calls received:", JSON.stringify(message.toolCalls));
72+
console.log("Tool call results:", JSON.stringify(message.toolResults));
73+
}
74+
},
75+
onError: (error) => {
76+
console.error("Error during chat response stream:", error);
77+
},
78+
maxSteps: 5,
79+
});
80+
console.log("Chat response stream initialized");
81+
82+
return response;
83+
}
84+
}

src/lib/ai/gemini.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
2+
import { ChatBot } from "./chat-bot";
3+
// import { google } from "@ai-sdk/google";
4+
5+
const API_KEY = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
6+
if (!API_KEY) {
7+
throw new Error("GOOGLE_GENERATIVE_AI_API_KEY is not set in the environment variables.");
8+
}
9+
10+
const modelName = "gemini-2.5-flash-preview-04-17";
11+
12+
const google = createGoogleGenerativeAI({
13+
apiKey: API_KEY,
14+
});
15+
const model = google(modelName);
16+
17+
// const globalForGemini = global as unknown as { gemini: ChatBot };
18+
19+
export const gemini = new ChatBot(model);
20+
// if (process.env.NODE_ENV === "development") globalForGemini.gemini = gemini;
21+
22+
export default gemini;

src/lib/ai/tools/mos-lookup.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { tool } from "ai";
2+
import { z } from "zod";
3+
import axios from "axios";
4+
import { chromium } from "@playwright/test";
5+
import { gemini } from "../gemini";
6+
7+
const { ONET_USER } = process.env;
8+
const { ONET_PASSWORD } = process.env;
9+
10+
export interface ONETResponse {
11+
keyword: string;
12+
branch: string;
13+
start: number;
14+
end: number;
15+
total: number;
16+
military_matches: MilitaryMatches;
17+
career: Career[];
18+
}
19+
20+
export interface MilitaryMatches {
21+
match: Match[];
22+
}
23+
24+
export interface Match {
25+
branch?: string;
26+
active: boolean;
27+
code: string;
28+
title: string;
29+
external_info?: ExternalInfo[];
30+
}
31+
32+
export interface ExternalInfo {
33+
href: string;
34+
title: string;
35+
}
36+
37+
export interface Career {
38+
href: string;
39+
match_type: string;
40+
code: string;
41+
title: string;
42+
tags: Tags;
43+
military_jobs: MilitaryJobs;
44+
}
45+
46+
export interface Tags {
47+
bright_outlook: boolean;
48+
green: boolean;
49+
apprenticeship: boolean;
50+
}
51+
52+
export interface MilitaryJobs {
53+
air_force: boolean;
54+
army: boolean;
55+
coast_guard: boolean;
56+
marine_corps: boolean;
57+
navy: boolean;
58+
}
59+
60+
const onetAPI = axios.create({
61+
baseURL: "https://services.onetcenter.org/v1.9/ws",
62+
headers: {
63+
"Content-Type": "application/json",
64+
Authorization: `Basic ${Buffer.from(`${ONET_USER}:${ONET_PASSWORD}`).toString("base64")}`,
65+
},
66+
});
67+
68+
const fetchExternalData = async (urls: string[]) => {
69+
const browser = await chromium.launch({
70+
headless: true,
71+
});
72+
const context = await browser.newContext({
73+
userAgent:
74+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
75+
});
76+
77+
const responses = await Promise.all(
78+
urls.map(async (url) => {
79+
const page = await context.newPage();
80+
await page.goto(url);
81+
return page.innerHTML("body");
82+
})
83+
);
84+
85+
await browser.close();
86+
87+
return responses;
88+
};
89+
90+
export const mosLookup = tool({
91+
description:
92+
"Lookup a Military Occupational Specialty (MOS) or Air Force Specialty (AFSC) code to get its description and related information. This tools should be used if the user enters the /mos or /afsc command which will be followed by the MOS or AFSC code. The tool will return information about the MOS of AFSC code. The data should be transformed into a user friendly format which includes skills that are transferrable to a career in tech. If the tool returns an error, the AI should respond with a relevant message to the user.",
93+
parameters: z.object({
94+
code: z.string().describe("The MOS or AFSC code to look up, e.g., '11B' for Infantryman."),
95+
}),
96+
execute: async ({ code }) => {
97+
const response = await onetAPI.get(`/veterans/military?keyword=${code}`);
98+
if (response.status !== 200) {
99+
return { error: `Failed to fetch MOS data for code: ${code}` };
100+
}
101+
try {
102+
const data = response.data as ONETResponse;
103+
if (
104+
!data ||
105+
!data.military_matches ||
106+
!data.military_matches.match ||
107+
data.military_matches.match.length === 0
108+
) {
109+
return { error: `No matches found for MOS code: ${code}` };
110+
}
111+
112+
if (data.military_matches.match.length > 1) {
113+
return { error: `Multiple matches found for MOS code: ${code}` };
114+
}
115+
116+
const match = data.military_matches.match[0];
117+
118+
// Fetch additional external data if available
119+
const externalURLs = match.external_info?.map((info) => info.href) || [];
120+
const externalData = await fetchExternalData(externalURLs);
121+
console.log("External data fetched:", externalData);
122+
123+
return {
124+
mos: match,
125+
similar_careers: data.career,
126+
mos_descriptions: externalData,
127+
};
128+
} catch (error) {
129+
return { error: `Failed to parse MOS data for code: ${code}` };
130+
}
131+
},
132+
});
133+
134+
export default mosLookup;

src/pages/chat.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { useChat } from "@ai-sdk/react";
4+
import { NextPage } from "next";
5+
import Layout from "@layout/layout-01";
6+
import { useSession, getSession } from "next-auth/react";
7+
import { useRouter } from "next/router";
8+
import { useEffect } from "react";
9+
10+
type PageWithLayoutType = NextPage & {
11+
Layout: typeof Layout;
12+
};
13+
14+
const Chat: PageWithLayoutType = () => {
15+
const { messages, input, handleInputChange, handleSubmit } = useChat();
16+
const { status } = useSession();
17+
const router = useRouter();
18+
19+
useEffect(() => {
20+
if (status === "unauthenticated") {
21+
router.push("/login");
22+
}
23+
}, [router, status]);
24+
25+
return (
26+
<div className="tw-stretch tw-mx-auto tw-flex tw-w-full tw-flex-col tw-items-center tw-justify-center tw-py-24">
27+
{messages.map((message) => (
28+
<div key={message.id} className="tw-w-full tw-whitespace-pre-wrap tw-px-10">
29+
{message.role === "user" ? "User: " : "AI: "}
30+
{message.parts.map((part, i) => {
31+
switch (part.type) {
32+
case "text":
33+
// eslint-disable-next-line react/no-array-index-key
34+
return <div key={`${message.id}-${i}`}>{part.text}</div>;
35+
default:
36+
return null;
37+
}
38+
})}
39+
</div>
40+
))}
41+
42+
<form onSubmit={handleSubmit}>
43+
<input
44+
className="tw-fixed tw-inset-x-0 tw-bottom-0 tw-z-50 tw-mx-auto tw-mb-8 tw-w-full tw-max-w-lg tw-rounded tw-border tw-border-zinc-300 tw-p-2 tw-shadow-xl dark:tw-border-zinc-800 dark:tw-bg-zinc-900"
45+
value={input}
46+
placeholder="Say something..."
47+
onChange={handleInputChange}
48+
/>
49+
</form>
50+
</div>
51+
);
52+
};
53+
54+
Chat.Layout = Layout;
55+
export default Chat;

0 commit comments

Comments
 (0)