Skip to content

Commit d01caff

Browse files
authored
Merge pull request #5 from nedhmn/feature/ai-rag-tool
Feature/ai rag tool
2 parents 6a12751 + 5c625ce commit d01caff

File tree

23 files changed

+337
-95
lines changed

23 files changed

+337
-95
lines changed

apps/web/app/api/chat/route.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { getRelevantRulings } from "@/lib/ai-tools";
12
import getOpenAIClient from "@repo/embeddings/openai";
2-
import { streamText } from "ai";
3+
import { streamText, tool } from "ai";
4+
import { z } from "zod";
5+
import { env } from "@/app/env";
36

47
export const maxDuration = 60;
58

@@ -9,8 +12,27 @@ export async function POST(req: Request) {
912
const openai = getOpenAIClient();
1013

1114
const result = streamText({
12-
model: openai.responses("gpt-4o-mini"),
15+
model: openai.responses(env.OPENAI_MODEL),
1316
messages,
17+
system: `
18+
You are a Yu-Gi-Oh! Goat Format ruling expert.
19+
You only answer questions about Yu-Gi-Oh! Goat Format rulings.
20+
The getInformation tool is mandatory for providing ruling context.
21+
The getInformation tool returns an array of objects containing the keys "title" and "url".
22+
Always link the URL as the source (with the title as the hyperlink text) for any ruling context you use.
23+
If the tool's results do not completely answer the question, respond with "Sorry, I don't know."
24+
Check your knowledge base before answering any questions.
25+
`,
26+
tools: {
27+
getInformation: tool({
28+
description:
29+
"Get information from your knowledge base to answer Yu-Gi-Oh! Goat Format rulings questions.",
30+
parameters: z.object({
31+
question: z.string().describe("the user's question"),
32+
}),
33+
execute: async ({ question }) => getRelevantRulings(question),
34+
}),
35+
},
1436
});
1537

1638
return result.toDataStreamResponse();
@@ -21,7 +43,7 @@ export async function POST(req: Request) {
2143
{
2244
status: 500,
2345
headers: { "Content-Type": "application/json" },
24-
},
46+
}
2547
);
2648
}
2749
}

apps/web/app/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const env = createEnv({
88
*/
99
server: {
1010
OPENAI_API_KEY: z.string(),
11+
OPENAI_MODEL: z.string().default("o4-mini-2025-04-16"),
1112
PINECONE_API_KEY: z.string(),
1213
PINECONE_INDEX_NAME: z.string(),
1314
},
@@ -24,6 +25,7 @@ export const env = createEnv({
2425
* 💡 You'll get type errors if not all variables from `server` & `client` are included here.
2526
*/
2627
runtimeEnv: {
28+
OPENAI_MODEL: process.env.OPENAI_MODEL,
2729
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
2830
PINECONE_API_KEY: process.env.PINECONE_API_KEY,
2931
PINECONE_INDEX_NAME: process.env.PINECONE_INDEX_NAME,

apps/web/app/globals.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@
22
@import "@repo/tailwind-config";
33
@import "@repo/ui/styles.css";
44

5+
@theme inline {
6+
--color-background: var(--background);
7+
--color-foreground: var(--foreground);
8+
--color-card: var(--card);
9+
--color-card-foreground: var(--card-foreground);
10+
--color-popover: var(--popover);
11+
--color-popover-foreground: var(--popover-foreground);
12+
--color-primary: var(--primary);
13+
--color-primary-foreground: var(--primary-foreground);
14+
--color-secondary: var(--secondary);
15+
--color-secondary-foreground: var(--secondary-foreground);
16+
--color-muted: var(--muted);
17+
--color-muted-foreground: var(--muted-foreground);
18+
--color-accent: var(--accent);
19+
--color-accent-foreground: var(--accent-foreground);
20+
--color-destructive: var(--destructive);
21+
--color-destructive-foreground: var(--destructive-foreground);
22+
--color-border: var(--border);
23+
--color-input: var(--input);
24+
--color-ring: var(--ring);
25+
--color-chart-1: var(--chart-1);
26+
--color-chart-2: var(--chart-2);
27+
--color-chart-3: var(--chart-3);
28+
--color-chart-4: var(--chart-4);
29+
--color-chart-5: var(--chart-5);
30+
--radius-sm: calc(var(--radius) - 4px);
31+
--radius-md: calc(var(--radius) - 2px);
32+
--radius-lg: var(--radius);
33+
--radius-xl: calc(var(--radius) + 4px);
34+
--color-sidebar: var(--sidebar);
35+
--color-sidebar-foreground: var(--sidebar-foreground);
36+
--color-sidebar-primary: var(--sidebar-primary);
37+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
38+
--color-sidebar-accent: var(--sidebar-accent);
39+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
40+
--color-sidebar-border: var(--sidebar-border);
41+
--color-sidebar-ring: var(--sidebar-ring);
42+
}
43+
44+
@layer base {
45+
* {
46+
@apply border-border outline-ring/50;
47+
}
48+
body {
49+
@apply bg-background text-foreground;
50+
}
51+
}
52+
553
/* Custom scrollbar styles */
654
::-webkit-scrollbar {
755
width: 8px;

apps/web/app/layout.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import Navbar from "@/components/navbar";
77
const inter = Inter({ subsets: ["latin"] });
88

99
export const metadata: Metadata = {
10-
title: "Create Turborepo",
11-
description: "Generated by create turbo",
10+
title: "Yu-Gi-Oh! Ruling Bot",
11+
description:
12+
"Get AI-generated information on Yu-Gi-Oh! Goat Format rulings using Retrieval Augmented Generation (RAG). A helpful tool for exploring card interactions and rules.",
1213
};
1314

1415
export default function RootLayout({

apps/web/components/chat-input-area.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,21 @@ const ChatInputArea = ({
2222
const { handleSendMessage } = useSendMessage({
2323
chatHelpers,
2424
isGeneratingResponse,
25-
input,
2625
setInput,
2726
});
2827

2928
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
3029
if (event.key === "Enter" && !event.shiftKey) {
3130
event.preventDefault();
32-
handleSendMessage();
31+
handleSendMessage(input);
3332
}
3433
};
3534

3635
return (
3736
<Textarea
3837
value={input}
3938
placeholder="How can I help with Yu-Gi-Oh! rulings?"
40-
className="pr-12 py-3 min-h-[60px] max-h-[120px] focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none border-0 resize-none"
39+
className="pr-12 p-5 min-h-[60px] max-h-[120px] focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none border-0 resize-none"
4140
disabled={isGeneratingResponse}
4241
autoFocus
4342
onChange={(event) => {

apps/web/components/chat-input-controls.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const ChatInputControls = ({
2222
const { handleSendMessage } = useSendMessage({
2323
chatHelpers,
2424
isGeneratingResponse,
25-
input,
2625
setInput,
2726
});
2827

@@ -31,7 +30,7 @@ const ChatInputControls = ({
3130
<Button
3231
size="icon"
3332
className="transform rounded-[10px] h-8 w-8 focus-visible:ring-1 focus-visible:ring-offset-1"
34-
onClick={handleSendMessage}
33+
onClick={() => handleSendMessage(input)}
3534
aria-label={isGeneratingResponse ? "Stop generating" : "Send message"}
3635
>
3736
{isGeneratingResponse ? <StopCircleIcon /> : <ArrowUpIcon />}

apps/web/components/chat-input.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
import { UseChatHelpers } from "@ai-sdk/react";
44
import ChatInputControls from "@/components/chat-input-controls";
55
import ChatInputArea from "@/components/chat-input-area";
6-
import { useState } from "react";
6+
import Footnote from "./footnote";
77

88
interface ChatInputProps {
99
chatHelpers: UseChatHelpers;
10+
input: string;
11+
setInput: (value: string) => void;
1012
}
1113

12-
const ChatInput = ({ chatHelpers }: ChatInputProps) => {
13-
const [input, setInput] = useState<string>("");
14-
14+
const ChatInput = ({ chatHelpers, input, setInput }: ChatInputProps) => {
1515
return (
1616
<div className="fixed bottom-0 left-0 right-0 bg-background py-4 z-10">
1717
<div className="max-w-3xl mx-auto w-full px-4">
@@ -27,10 +27,7 @@ const ChatInput = ({ chatHelpers }: ChatInputProps) => {
2727
setInput={setInput}
2828
/>
2929
</div>
30-
<div className="text-muted-foreground text-xs mt-2">
31-
This is an AI chatbot using RAG. It is trained on GOAT Format
32-
Yu-Gi-Oh! rulings only.
33-
</div>
30+
<Footnote />
3431
</div>
3532
</div>
3633
);

apps/web/components/chat-messages.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { UIMessage } from "ai";
21
import { useEffect, useRef } from "react";
32
import Markdown from "react-markdown";
43
import { markdownComponents } from "@repo/ui/components/markdown-components";
54
import { cn } from "@repo/ui/lib/utils";
5+
import StartMessages from "./start-messages";
6+
import { UseChatHelpers } from "@ai-sdk/react";
67

78
type ChatMessagesProps = {
8-
messages: Array<UIMessage>;
9+
chatHelpers: UseChatHelpers;
10+
setInput: (value: string) => void;
911
};
1012

11-
const ChatMessages = ({ messages }: ChatMessagesProps) => {
13+
const ChatMessages = ({ chatHelpers, setInput }: ChatMessagesProps) => {
1214
const messagesRef = useRef<HTMLDivElement>(null);
15+
const { messages } = chatHelpers;
1316

1417
useEffect(() => {
1518
if (messagesRef.current) {
@@ -24,27 +27,23 @@ const ChatMessages = ({ messages }: ChatMessagesProps) => {
2427
<div className="max-w-3xl mx-auto w-full px-4 pt-20">
2528
<div className="py-4">
2629
{messages.length === 0 ? (
27-
<div className="flex items-center justify-center min-h-[70vh]">
28-
<p className="text-foreground text-lg">
29-
{"Hey there! What's on your mind?"}
30-
</p>
31-
</div>
30+
<StartMessages chatHelpers={chatHelpers} setInput={setInput} />
3231
) : (
3332
<div className="space-y-6">
3433
{messages.map((message) => (
3534
<div
3635
key={message.id}
3736
className={cn(
3837
"flex",
39-
message.role === "user" ? "justify-end" : "justify-start",
38+
message.role === "user" ? "justify-end" : "justify-start"
4039
)}
4140
>
4241
<div
4342
className={cn(
4443
"rounded-lg px-4 py-2 max-w-[80%]",
4544
message.role === "user"
4645
? "bg-primary text-primary-foreground"
47-
: "bg-secondary text-foreground system-message-styles",
46+
: "bg-secondary text-foreground system-message-styles"
4847
)}
4948
>
5049
<Markdown components={markdownComponents}>

apps/web/components/chat.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@ import ChatMessages from "@/components/chat-messages";
44
import { useChat } from "@ai-sdk/react";
55
import ChatInput from "@/components/chat-input";
66
import { toast } from "sonner";
7+
import { useState } from "react";
78

89
const Chat = () => {
10+
const [input, setInput] = useState<string>("");
11+
912
const chatHelpers = useChat({
1013
id: "primary",
14+
maxSteps: 3,
1115
onError: () => {
1216
toast.error("An error occurred, please try again!");
1317
},
1418
});
1519

16-
const { messages } = chatHelpers;
17-
1820
return (
1921
<div className="min-h-screen bg-background pb-24">
20-
<ChatMessages messages={messages}></ChatMessages>
21-
<ChatInput chatHelpers={chatHelpers} />
22+
<ChatMessages chatHelpers={chatHelpers} setInput={setInput} />
23+
<ChatInput chatHelpers={chatHelpers} input={input} setInput={setInput} />
2224
</div>
2325
);
2426
};

apps/web/components/footnote.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const Footnote = () => {
2+
return (
3+
<div className="text-muted-foreground text-xs mt-2">
4+
This is an AI chatbot using{" "}
5+
<a
6+
className="underline text-primary"
7+
href="https://www.pinecone.io/learn/retrieval-augmented-generation/"
8+
target="_blank"
9+
rel="noreferrer"
10+
>
11+
RAG
12+
</a>
13+
. It is trained on{" "}
14+
<a
15+
className="underline text-primary"
16+
href="https://www.goatformat.com/whatisgoat.html"
17+
target="_blank"
18+
rel="noreferrer"
19+
>
20+
GOAT Format Yu-Gi-Oh!
21+
</a>{" "}
22+
rulings only.
23+
</div>
24+
);
25+
};
26+
27+
export default Footnote;

0 commit comments

Comments
 (0)