├── agent-complete ├── .env.example ├── src │ └── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── api │ │ └── agent │ │ ├── tools.ts │ │ └── route.ts ├── postcss.config.mjs ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg ├── next.config.ts ├── eslint.config.mjs ├── .gitignore ├── package.json ├── tsconfig.json └── README.md ├── agent-starter ├── .env.example ├── postcss.config.mjs ├── src │ └── app │ │ ├── favicon.ico │ │ ├── api │ │ └── agent │ │ │ ├── tools.ts │ │ │ └── route.ts │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── snippets.md ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg ├── next.config.ts ├── eslint.config.mjs ├── .gitignore ├── package.json ├── tsconfig.json └── README.md └── README.md /agent-complete/.env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env.local and add your key 2 | OPENAI_API_KEY=sk-your-key-here 3 | -------------------------------------------------------------------------------- /agent-starter/.env.example: -------------------------------------------------------------------------------- 1 | # Copy this file to .env.local and add your key 2 | OPENAI_API_KEY=sk-your-key-here 3 | -------------------------------------------------------------------------------- /agent-complete/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspruance/ai-agent-tutorial/main/agent-complete/src/app/favicon.ico -------------------------------------------------------------------------------- /agent-starter/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /agent-starter/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspruance/ai-agent-tutorial/main/agent-starter/src/app/favicon.ico -------------------------------------------------------------------------------- /agent-complete/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /agent-complete/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-starter/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-complete/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /agent-starter/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /agent-starter/src/app/api/agent/tools.ts: -------------------------------------------------------------------------------- 1 | // app/api/agent/tools.ts 2 | 3 | import type { ChatCompletionTool } from "openai/resources/chat/completions"; 4 | 5 | // Tools available to the AI agent 6 | export const tools: ChatCompletionTool[] = []; 7 | 8 | // Tool implementations (to be added later) 9 | export async function runTool(name: string, args: any) { 10 | return "Unknown tool"; 11 | } 12 | -------------------------------------------------------------------------------- /agent-complete/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-starter/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-complete/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-starter/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-complete/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /agent-starter/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /agent-complete/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | ignores: [ 16 | "node_modules/**", 17 | ".next/**", 18 | "out/**", 19 | "build/**", 20 | "next-env.d.ts", 21 | ], 22 | }, 23 | ]; 24 | 25 | export default eslintConfig; 26 | -------------------------------------------------------------------------------- /agent-starter/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | ignores: [ 16 | "node_modules/**", 17 | ".next/**", 18 | "out/**", 19 | "build/**", 20 | "next-env.d.ts", 21 | ], 22 | }, 23 | ]; 24 | 25 | export default eslintConfig; 26 | -------------------------------------------------------------------------------- /agent-complete/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | !.env.example 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /agent-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | !.env.example 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /agent-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint" 10 | }, 11 | "dependencies": { 12 | "next": "15.5.4", 13 | "openai": "^5.23.1", 14 | "react": "19.1.0", 15 | "react-dom": "19.1.0" 16 | }, 17 | "devDependencies": { 18 | "@eslint/eslintrc": "^3", 19 | "@tailwindcss/postcss": "^4", 20 | "@types/node": "^20", 21 | "@types/react": "^19", 22 | "@types/react-dom": "^19", 23 | "eslint": "^9", 24 | "eslint-config-next": "15.5.4", 25 | "tailwindcss": "^4", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /agent-complete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-complete", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint" 10 | }, 11 | "dependencies": { 12 | "next": "15.5.4", 13 | "openai": "^5.23.1", 14 | "react": "19.1.0", 15 | "react-dom": "19.1.0" 16 | }, 17 | "devDependencies": { 18 | "@eslint/eslintrc": "^3", 19 | "@tailwindcss/postcss": "^4", 20 | "@types/node": "^20", 21 | "@types/react": "^19", 22 | "@types/react-dom": "^19", 23 | "eslint": "^9", 24 | "eslint-config-next": "15.5.4", 25 | "tailwindcss": "^4", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /agent-complete/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /agent-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /agent-complete/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /agent-starter/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /agent-starter/src/app/api/agent/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/agent/route.ts 2 | import { NextResponse } from "next/server"; 3 | import OpenAI from "openai"; 4 | import { tools, runTool } from "./tools"; 5 | 6 | const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); 7 | 8 | export async function POST(req: Request) { 9 | try { 10 | const { query } = await req.json(); 11 | // TODO: Call OpenAI with tools 12 | // Step 1: Ask GPT with tools 13 | // Step 2: If GPT calls a tool → run it 14 | // Step 3: Send result back for reasoning 15 | // Step 4: Return final answer 16 | 17 | // Placeholder response until the agent logic is implemented 18 | return NextResponse.json({ answer: "Agent not implemented yet" }); 19 | } catch (err: any) { 20 | console.error("API error:", err); 21 | return NextResponse.json( 22 | { error: err.message || "Server error" }, 23 | { status: 500 } 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /agent-complete/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-starter/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-complete/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent-starter/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 OpenAI Agent Tutorial – Starter Repo 2 | 3 | This is the **starter code** for my YouTube tutorial: 4 | 👉 _“Your First AI Agent (Next.js + OpenAI Tool Calling)”_ 5 | 6 | We’ll start from a fresh **Next.js 15 project** with: 7 | 8 | - ✅ TypeScript 9 | - ✅ Tailwind CSS 10 | - ✅ App Router 11 | - ✅ `@/*` import alias 12 | 13 | In the tutorial, we’ll add the code that turns this into a working **AI Agent**. 14 | 15 | --- 16 | 17 | ## 📦 Setup 18 | 19 | Clone this repo: 20 | 21 | ```bash 22 | git clone https://github.com/jspruance/ai-agent-tutorial.git 23 | cd openai-agent-tutorial-starter 24 | ``` 25 | 26 | Install dependencies: 27 | 28 | ```bash 29 | npm install 30 | ``` 31 | 32 | Add your OpenAI API key (not yet used, but needed later): 33 | 34 | ```bash 35 | # .env.local 36 | OPENAI_API_KEY=your_api_key_here 37 | ``` 38 | 39 | --- 40 | 41 | ## ▶️ Run the Dev Server 42 | 43 | ```bash 44 | npm run dev 45 | ``` 46 | 47 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 48 | 49 | You should see a **blank Next.js app** with Tailwind installed. 50 | 51 | --- 52 | 53 | ## 🗂️ Project Structure 54 | 55 | ``` 56 | src/ 57 | └── app/ 58 | ├── page.tsx # Simple placeholder page 59 | └── api/ # Agent code will go here in the tutorial 60 | ``` 61 | 62 | --- 63 | 64 | ## 📚 References 65 | 66 | - [Next.js App Router Docs](https://nextjs.org/docs/app) 67 | - [Tailwind CSS Docs](https://tailwindcss.com/docs/guides/nextjs) 68 | - [OpenAI Docs](https://platform.openai.com/docs/guides/function-calling) 69 | 70 | --- 71 | 72 | 👉 This repo is just the **starting point**. 73 | Follow the tutorial to build out your **first AI Agent** step by step! 74 | -------------------------------------------------------------------------------- /agent-starter/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 OpenAI Agent Tutorial – Starter Repo 2 | 3 | This is the **starter code** for my YouTube tutorial: 4 | 👉 _“Your First AI Agent (Next.js + OpenAI Tool Calling)”_ 5 | 6 | We’ll start from a fresh **Next.js 15 project** with: 7 | 8 | - ✅ TypeScript 9 | - ✅ Tailwind CSS 10 | - ✅ App Router 11 | - ✅ `@/*` import alias 12 | 13 | In the tutorial, we’ll add the code that turns this into a working **AI Agent**. 14 | 15 | --- 16 | 17 | ## 📦 Setup 18 | 19 | Clone this repo: 20 | 21 | ```bash 22 | git clone https://github.com/jspruance/ai-agent-tutorial.git 23 | cd openai-agent-tutorial-starter 24 | ``` 25 | 26 | Install dependencies: 27 | 28 | ```bash 29 | npm install 30 | ``` 31 | 32 | Add your OpenAI API key (not yet used, but needed later): 33 | 34 | ```bash 35 | # .env.local 36 | OPENAI_API_KEY=your_api_key_here 37 | ``` 38 | 39 | --- 40 | 41 | ## ▶️ Run the Dev Server 42 | 43 | ```bash 44 | npm run dev 45 | ``` 46 | 47 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 48 | 49 | You should see a **blank Next.js app** with Tailwind installed. 50 | 51 | --- 52 | 53 | ## 🗂️ Project Structure 54 | 55 | ``` 56 | src/ 57 | └── app/ 58 | ├── page.tsx # Simple placeholder page 59 | └── api/ # Agent code will go here in the tutorial 60 | ``` 61 | 62 | --- 63 | 64 | ## 📚 References 65 | 66 | - [Next.js App Router Docs](https://nextjs.org/docs/app) 67 | - [Tailwind CSS Docs](https://tailwindcss.com/docs/guides/nextjs) 68 | - [OpenAI Docs](https://platform.openai.com/docs/guides/function-calling) 69 | 70 | --- 71 | 72 | 👉 This repo is just the **starting point**. 73 | Follow the tutorial to build out your **first AI Agent** step by step! 74 | -------------------------------------------------------------------------------- /agent-starter/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | // app/page.tsx 2 | "use client"; 3 | import { useState } from "react"; 4 | 5 | export default function Home() { 6 | const [query, setQuery] = useState(""); 7 | const [answer, setAnswer] = useState(""); 8 | const [loading, setLoading] = useState(false); 9 | 10 | const askAgent = async () => { 11 | // TODO: Call our /api/agent endpoint 12 | setAnswer("Agent not connected yet"); 13 | }; 14 | 15 | return ( 16 |
17 |
18 |

19 | 🤖 AI Agent Demo 20 |

21 |

22 | Ask me math questions, check the weather, or even get a programming 23 | joke! 24 |

25 | 26 |
27 | setQuery(e.target.value)} 31 | placeholder="Type your question here..." 32 | /> 33 | 40 |
41 | 42 | {answer && ( 43 |
44 |

{answer}

45 |
46 | )} 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /agent-complete/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | 4 | export default function Home() { 5 | const [query, setQuery] = useState(""); 6 | const [answer, setAnswer] = useState(""); 7 | const [loading, setLoading] = useState(false); 8 | 9 | const askAgent = async () => { 10 | setLoading(true); 11 | setAnswer(""); 12 | const res = await fetch("/api/agent", { 13 | method: "POST", 14 | headers: { "Content-Type": "application/json" }, 15 | body: JSON.stringify({ query }), 16 | }); 17 | const data = await res.json(); 18 | setAnswer(data.answer || "No response"); 19 | setLoading(false); 20 | }; 21 | 22 | return ( 23 |
24 |
25 |

26 | 🤖 AI Agent Demo 27 |

28 |

29 | Ask me math questions, check the weather, or even get a programming 30 | joke! 31 |

32 | 33 |
34 | setQuery(e.target.value)} 38 | placeholder="Type your question here..." 39 | /> 40 | 47 |
48 | 49 | {answer && ( 50 |
51 |

{answer}

52 |
53 | )} 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /agent-complete/src/app/api/agent/tools.ts: -------------------------------------------------------------------------------- 1 | import type { ChatCompletionTool } from "openai/resources/chat/completions"; 2 | 3 | // Tool definitions (what GPT sees) 4 | export const tools: ChatCompletionTool[] = [ 5 | { 6 | type: "function", 7 | function: { 8 | name: "calculator", 9 | description: "Evaluate basic math expressions", 10 | parameters: { 11 | type: "object", 12 | properties: { 13 | expression: { type: "string" }, 14 | }, 15 | required: ["expression"], 16 | }, 17 | }, 18 | }, 19 | { 20 | type: "function", 21 | function: { 22 | name: "getWeather", 23 | description: "Get the weather for a given location and date", 24 | parameters: { 25 | type: "object", 26 | properties: { 27 | location: { type: "string" }, 28 | date: { type: "string" }, 29 | }, 30 | required: ["location", "date"], 31 | }, 32 | }, 33 | }, 34 | { 35 | type: "function", 36 | function: { 37 | name: "tellJoke", 38 | description: "Return a random programming joke", 39 | parameters: { 40 | type: "object", 41 | properties: {}, 42 | }, 43 | }, 44 | }, 45 | ]; 46 | 47 | // Tool implementations (how they actually work) 48 | export async function runTool(name: string, args: any) { 49 | if (name === "calculator") { 50 | try { 51 | // ⚠️ demo only — do NOT use eval in production 52 | // eslint-disable-next-line no-eval 53 | return eval(args.expression); 54 | } catch { 55 | return "Error evaluating expression"; 56 | } 57 | } 58 | 59 | if (name === "getWeather") { 60 | // Mocked response — replace with a real API call if needed 61 | return { forecast: "Rainy", temperature: "15°C" }; 62 | } 63 | 64 | if (name === "tellJoke") { 65 | const jokes = [ 66 | "Why do programmers prefer dark mode? Because light attracts bugs.", 67 | "There are 10 types of people in the world: those who understand binary and those who don’t.", 68 | "I would tell you a UDP joke, but you might not get it.", 69 | ]; 70 | return jokes[Math.floor(Math.random() * jokes.length)]; 71 | } 72 | 73 | return "Unknown tool"; 74 | } 75 | -------------------------------------------------------------------------------- /agent-complete/src/app/api/agent/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import OpenAI from "openai"; 3 | import { tools, runTool } from "./tools"; 4 | 5 | const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); 6 | 7 | export async function POST(req: Request) { 8 | try { 9 | if (!process.env.OPENAI_API_KEY) { 10 | return NextResponse.json( 11 | { error: "Missing OPENAI_API_KEY" }, 12 | { status: 500 } 13 | ); 14 | } 15 | 16 | const { query } = await req.json(); 17 | console.log("Incoming query:", query); 18 | 19 | // 1) First request: ask GPT with tools available 20 | const first = await client.chat.completions.create({ 21 | model: "gpt-4.1", 22 | messages: [{ role: "user", content: query }], 23 | tools, 24 | }); 25 | 26 | const msg = first.choices[0].message; 27 | 28 | // 2) If GPT decides to call a tool 29 | if (msg.tool_calls?.length) { 30 | const results = await Promise.all( 31 | msg.tool_calls.map(async (call: any) => { 32 | const args = JSON.parse(call.function.arguments || "{}"); 33 | const result = await runTool(call.function.name, args); 34 | return { 35 | tool_call_id: call.id, 36 | name: call.function.name, 37 | result, 38 | }; 39 | }) 40 | ); 41 | 42 | // 3) Send tool outputs back for final reasoning 43 | const final = await client.chat.completions.create({ 44 | model: "gpt-4.1", 45 | messages: [ 46 | { role: "user", content: query }, 47 | msg, 48 | ...results.map((r) => ({ 49 | role: "tool" as const, 50 | tool_call_id: r.tool_call_id, 51 | content: JSON.stringify(r.result), 52 | })), 53 | ], 54 | }); 55 | 56 | return NextResponse.json({ 57 | answer: final.choices[0].message?.content || "(no answer)", 58 | }); 59 | } 60 | 61 | // 4) If no tools needed 62 | return NextResponse.json({ answer: msg.content }); 63 | } catch (err: any) { 64 | console.error("API error:", err); 65 | return NextResponse.json( 66 | { error: err.message || "Server error" }, 67 | { status: 500 } 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /agent-complete/README.md: -------------------------------------------------------------------------------- 1 | # 🤖 OpenAI Agent Tutorial – Final Project 2 | 3 | This is the **completed code** from my YouTube tutorial: 4 | 👉 _“Your First AI Agent (Next.js + OpenAI Tool Calling)”_ 5 | 6 | We built a simple AI Agent that can: 7 | 8 | - Answer normal chat questions 9 | - Call a **calculator tool** for math 10 | - Call a **weather tool** for forecasts 11 | - Combine results into a final smart answer 12 | 13 | --- 14 | 15 | ## 📦 Setup 16 | 17 | Clone this repo: 18 | 19 | ```bash 20 | git clone https://github.com/jspruance/ai-agent-tutorial.git 21 | cd openai-agent-tutorial-final 22 | ``` 23 | 24 | Install dependencies: 25 | 26 | ```bash 27 | npm install 28 | ``` 29 | 30 | Add your OpenAI API key: 31 | 32 | ```bash 33 | # .env.local 34 | OPENAI_API_KEY=your_api_key_here 35 | ``` 36 | 37 | --- 38 | 39 | ## ▶️ Run the Dev Server 40 | 41 | ```bash 42 | npm run dev 43 | ``` 44 | 45 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 46 | 47 | --- 48 | 49 | ## 🗂️ Project Structure 50 | 51 | ``` 52 | src/ 53 | └── app/ 54 | ├── page.tsx # Simple frontend UI 55 | └── api/ 56 | └── agent/ 57 | ├── tools.ts # Tool definitions + implementations 58 | └── route.ts # API route (calls OpenAI + tools) 59 | ``` 60 | 61 | --- 62 | 63 | ## 🛠️ Tools Implemented 64 | 65 | ### 🔢 Calculator 66 | 67 | - Evaluates basic math expressions 68 | - Example: `25 * 17` → `425` 69 | 70 | ### 🌦️ Weather (Mock) 71 | 72 | - Returns a fake forecast (replace with real API later) 73 | - Example: `Paris tomorrow` → `{ forecast: "Rainy", temperature: "15°C" }` 74 | 75 | --- 76 | 77 | ## 🎮 Demo Scenarios 78 | 79 | 1. **Math Only** 80 | 81 | ``` 82 | What is 25 times 17? 83 | ``` 84 | 85 | ✅ Agent calls calculator tool 86 | 87 | 2. **Weather Only** 88 | 89 | ``` 90 | What’s the weather tomorrow in Paris? 91 | ``` 92 | 93 | ✅ Agent calls weather tool 94 | 95 | 3. **Multi-step Reasoning** 96 | ``` 97 | If 25 * 17 is X, and it’s rainy tomorrow in Paris, should I bring an umbrella? 98 | ``` 99 | ✅ Agent calls **both tools** and combines results 100 | 101 | --- 102 | 103 | ## 📚 References 104 | 105 | - [OpenAI Tool Calling Docs](https://platform.openai.com/docs/guides/function-calling) 106 | - [Next.js App Router](https://nextjs.org/docs/app) 107 | - [Tailwind CSS](https://tailwindcss.com/docs/guides/nextjs) 108 | 109 | --- 110 | 111 | ## 🌟 Extend the Agent 112 | 113 | Want to keep going? Add new tools: 114 | 115 | - 📈 Currency converter (exchange rates) 116 | - 😂 Joke generator 117 | - 🔍 Web search 118 | - 🗂️ Knowledge base lookup 119 | 120 | --- 121 | 122 | 👉 By the end of the tutorial, you’ll have a **working AI Agent** you can extend into your own SaaS projects. 123 | -------------------------------------------------------------------------------- /agent-starter/src/app/snippets.md: -------------------------------------------------------------------------------- 1 | # 📄 Code Snippets for AI Agent Demo 2 | 3 | ```ts 4 | // 🔹 Tools 5 | // Calculator tool 6 | { 7 | type: "function", 8 | function: { 9 | name: "calculator", 10 | description: "Evaluate basic math expressions", 11 | parameters: { 12 | type: "object", 13 | properties: { 14 | expression: { type: "string" }, 15 | }, 16 | required: ["expression"], 17 | }, 18 | }, 19 | } 20 | 21 | // Calculator tool implementation 22 | if (name === "calculator") { 23 | try { 24 | // ⚠️ demo only — eval is unsafe in production 25 | // eslint-disable-next-line no-eval 26 | return eval(args.expression); 27 | } catch { 28 | return "Error evaluating expression"; 29 | } 30 | } 31 | 32 | 33 | // Weather Tool Definition 34 | { 35 | type: "function", 36 | function: { 37 | name: "getWeather", 38 | description: "Get the weather for a given location and date", 39 | parameters: { 40 | type: "object", 41 | properties: { 42 | location: { type: "string" }, 43 | date: { type: "string" }, 44 | }, 45 | required: ["location", "date"], 46 | }, 47 | }, 48 | } 49 | 50 | // Joke Tool Definition 51 | { 52 | type: "function", 53 | function: { 54 | name: "tellJoke", 55 | description: "Return a random programming joke", 56 | parameters: { 57 | type: "object", 58 | properties: {}, 59 | }, 60 | }, 61 | } 62 | 63 | // Weather + Joke Implementations 64 | if (name === "getWeather") { 65 | return { forecast: "Rainy", temperature: "15°C" }; 66 | } 67 | 68 | if (name === "tellJoke") { 69 | const jokes = [ 70 | "Why do programmers prefer dark mode? Because light attracts bugs.", 71 | "There are 10 types of people in the world: those who understand binary and those who don’t.", 72 | "I would tell you a UDP joke, but you might not get it.", 73 | ]; 74 | return jokes[Math.floor(Math.random() * jokes.length)]; 75 | } 76 | 77 | // 🔹 API Route Logic (replace placeholder in route.ts) 78 | 79 | // 1) First request: ask GPT with tools 80 | const first = await client.chat.completions.create({ 81 | model: "gpt-4.1", 82 | messages: [{ role: "user", content: query }], 83 | tools, 84 | }); 85 | 86 | const msg = first.choices[0].message; 87 | // optional logging 88 | console.log("Tool calls:", msg.tool_calls); 89 | 90 | // 2) If GPT decides to call a tool 91 | if (msg.tool_calls?.length) { 92 | const results = await Promise.all( 93 | msg.tool_calls.map(async (call: any) => { 94 | const args = JSON.parse(call.function.arguments || "{}"); 95 | const result = await runTool(call.function.name, args); 96 | return { tool_call_id: call.id, result }; 97 | }) 98 | ); 99 | 100 | // 3) Send tool outputs back for final reasoning 101 | const final = await client.chat.completions.create({ 102 | model: "gpt-4.1", 103 | messages: [ 104 | { role: "user", content: query }, 105 | msg, 106 | ...results.map((r) => ({ 107 | role: "tool" as const, 108 | tool_call_id: r.tool_call_id, 109 | content: JSON.stringify(r.result), 110 | })), 111 | ], 112 | }); 113 | 114 | return NextResponse.json({ 115 | answer: final.choices[0].message?.content || "(no answer)", 116 | }); 117 | } 118 | 119 | // 4) If no tools needed 120 | return NextResponse.json({ answer: msg.content }); 121 | 122 | // 🔹 Frontend Fetch Logic (replace placeholder in page.tsx) 123 | const askAgent = async () => { 124 | setAnswer(""); 125 | setLoading(true); 126 | try { 127 | const res = await fetch("/api/agent", { 128 | method: "POST", 129 | headers: { "Content-Type": "application/json" }, 130 | body: JSON.stringify({ query }), 131 | }); 132 | const data = await res.json(); 133 | setAnswer(data.answer || data.error || "(no answer)"); 134 | } catch (err) { 135 | console.error(err); 136 | setAnswer("❌ Something went wrong"); 137 | } finally { 138 | setLoading(false); 139 | } 140 | }; 141 | 142 | 143 | // 🔹 Input + Button (UI in page.tsx) 144 | 145 | setQuery(e.target.value)} 149 | placeholder="Ask me something..." 150 | /> 151 | 157 | {answer &&

🤖 {answer}

} 158 | ``` 159 | --------------------------------------------------------------------------------