├── README.md ├── challenge1 ├── frontend │ ├── .eslintrc.json │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ └── get_response │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── assistant.tsx │ │ ├── chat.tsx │ │ ├── message.css │ │ ├── message.tsx │ │ └── tool-call.tsx │ ├── lib │ │ ├── assistant.ts │ │ ├── constants.ts │ │ ├── tools.ts │ │ └── utils.ts │ ├── next.config.mjs │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── prettier.config.cjs │ ├── public │ │ └── imgs │ │ │ └── convex_icon.svg │ ├── stores │ │ └── useConversationStore.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── package-lock.json └── python-backend │ ├── .env.example │ ├── .gitignore │ ├── app.py │ └── requirements.txt ├── challenge2 ├── frontend │ ├── .eslintrc.json │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── get_response │ │ │ │ └── route.ts │ │ │ └── search_location │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── assistant.tsx │ │ ├── chat.tsx │ │ ├── message.css │ │ ├── message.tsx │ │ └── tool-call.tsx │ ├── lib │ │ ├── assistant.ts │ │ ├── constants.ts │ │ ├── tools.ts │ │ └── utils.ts │ ├── next.config.mjs │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── prettier.config.cjs │ ├── public │ │ └── imgs │ │ │ └── convex_icon.svg │ ├── stores │ │ └── useConversationStore.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── package-lock.json └── python-backend │ ├── .env.example │ ├── .gitignore │ ├── app.py │ └── requirements.txt ├── challenge3 ├── frontend │ ├── .eslintrc.json │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── create_itinerary │ │ │ │ └── route.ts │ │ │ ├── get_response │ │ │ │ └── route.ts │ │ │ └── search_location │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── assistant.tsx │ │ ├── chat.tsx │ │ ├── message.css │ │ ├── message.tsx │ │ └── tool-call.tsx │ ├── lib │ │ ├── assistant.ts │ │ ├── constants.ts │ │ ├── tools.ts │ │ └── utils.ts │ ├── next.config.mjs │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── prettier.config.cjs │ ├── public │ │ └── imgs │ │ │ └── convex_icon.svg │ ├── stores │ │ └── useConversationStore.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── package-lock.json └── python-backend │ ├── .env.example │ ├── .gitignore │ ├── app.py │ └── requirements.txt ├── challenge4 ├── frontend │ ├── .eslintrc.json │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── create_itinerary │ │ │ │ └── route.ts │ │ │ ├── get_response │ │ │ │ └── route.ts │ │ │ ├── search_location │ │ │ │ └── route.ts │ │ │ └── session │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── assistant.tsx │ │ ├── chat.tsx │ │ ├── message.css │ │ ├── message.tsx │ │ ├── tool-call.tsx │ │ └── voice-mode.tsx │ ├── lib │ │ ├── assistant.ts │ │ ├── constants.ts │ │ ├── tools.ts │ │ └── utils.ts │ ├── next.config.mjs │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── prettier.config.cjs │ ├── public │ │ └── imgs │ │ │ └── convex_icon.svg │ ├── stores │ │ └── useConversationStore.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── package-lock.json └── python-backend │ ├── .env.example │ ├── .gitignore │ ├── app.py │ └── requirements.txt ├── challenge5 ├── frontend │ ├── .eslintrc.json │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── create_itinerary │ │ │ │ └── route.ts │ │ │ ├── get_response │ │ │ │ └── route.ts │ │ │ ├── search_location │ │ │ │ └── route.ts │ │ │ └── session │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── assistant.tsx │ │ ├── chat.tsx │ │ ├── message.css │ │ ├── message.tsx │ │ ├── tool-call.tsx │ │ ├── ui │ │ │ ├── button.tsx │ │ │ ├── chart.tsx │ │ │ └── table.tsx │ │ └── voice-mode.tsx │ ├── lib │ │ ├── assistant.ts │ │ ├── constants.ts │ │ ├── tools.ts │ │ └── utils.ts │ ├── next.config.mjs │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── prettier.config.cjs │ ├── public │ │ ├── imgs │ │ │ ├── acc-MLC82.webp │ │ │ ├── cl-CE7902.webp │ │ │ ├── convex_icon.svg │ │ │ ├── mb-NE9000.webp │ │ │ └── qt-24X553.webp │ │ └── screenshot.jpg │ ├── stores │ │ └── useConversationStore.ts │ ├── tailwind.config.ts │ └── tsconfig.json └── python-backend │ ├── .env.example │ ├── .gitignore │ ├── app.py │ └── requirements.txt ├── license.txt └── starting_point ├── frontend ├── .eslintrc.json ├── .gitignore ├── app │ ├── api │ │ └── get_response │ │ │ └── route.ts │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components.json ├── components │ ├── assistant.tsx │ ├── chat.tsx │ ├── message.css │ ├── message.tsx │ └── tool-call.tsx ├── lib │ ├── assistant.ts │ ├── constants.ts │ ├── tools.ts │ └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prettier.config.cjs ├── public │ └── imgs │ │ └── convex_icon.svg ├── stores │ └── useConversationStore.ts ├── tailwind.config.ts └── tsconfig.json ├── package-lock.json └── python-backend ├── .env.example ├── .gitignore ├── app.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI Builder Lab 2 | 3 | This repository contains a starter project to help you go through the Builder Lab challenges faster. 4 | 5 | It consists of a NextJS application, in the `frontend` folder, and an optional python backend in the `python-backend` folder. 6 | You can use the built-in back-end in the NextJS application (`/api/route`), or change the endpoint to point to the python backend if you prefer using python. 7 | 8 | ## How to use 9 | 10 | 1. Download or clone this repository: 11 | 12 | ```bash 13 | git clone https://github.com/openai/openai-builder-lab.git 14 | ``` 15 | 16 | 2. Navigate to the starting point NextJS app: 17 | 18 | ```bash 19 | cd starting_point/frontend 20 | ``` 21 | 22 | 3. Set your OpenAI API key: 23 | 24 | 2 options: 25 | 26 | - Set the `OPENAI_API_KEY` environment variable [globally in your system](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key) 27 | - Set the `OPENAI_API_KEY` environment variable in the project: Create a `.env` file at the root of the project and add the following line: 28 | 29 | ``` 30 | OPENAI_API_KEY= 31 | ``` 32 | 33 | 4. Install dependencies: 34 | 35 | ```bash 36 | npm install 37 | ``` 38 | 39 | 5. Run the application: 40 | 41 | ```bash 42 | npm run dev 43 | ``` 44 | 45 | 6. (Optional) Run the python backend: 46 | 47 | a. Navigate to the `python-backend` folder: 48 | 49 | ```bash 50 | cd ../python-backend 51 | ``` 52 | 53 | b. Install dependencies: 54 | 55 | ```bash 56 | pip install -r requirements.txt 57 | ``` 58 | 59 | c. Run the backend: 60 | 61 | ```bash 62 | python app.py 63 | ``` 64 | 65 | 7. Start the challenges: 66 | 67 | Go through the lab by adding gradually the code needed for each challenge. 68 | Feel free to ask our team for help at any time! 69 | 70 | ## App structure 71 | 72 | ### UI 73 | 74 | The frontend part of the application lives in `frontend/components`. 75 | If you want to customize the UI, you can do so by editing the files in this folder or adding new components. 76 | 77 | ### Code Logic 78 | 79 | All the code logic lives in `frontend/lib`. 80 | This is where the logic related to making the assistant work is. 81 | You can edit those files, following the suggested structure, to progress through the challenges. 82 | 83 | For example: 84 | 85 | - `constants.ts` contains all constants such as system prompt, initial message, etc. 86 | - `tools.ts` is meant to contain logic related to the tools the assistant has access to. 87 | - `assistant.ts` contains logic to send and receive messages to the API via the backend. 88 | 89 | The messages are stored in a store shared across components, located in `frontend/stores/useConversationStore.ts`. You shouldn't have to edit this file. 90 | 91 | ### Backend 92 | 93 | The backend routes are located in `frontend/app/api`, or `python-backend/app.py`. 94 | 95 | This is where you can communicate with APIs, including OpenAI APIs. 96 | Feel free to add more routes as you progress through the challenges. 97 | 98 | The first route `get_response` is provided as an example, and you can follow the same structure to add more routes. 99 | -------------------------------------------------------------------------------- /challenge1/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /challenge1/frontend/.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /challenge1/frontend/app/api/get_response/route.ts: -------------------------------------------------------------------------------- 1 | import { MODEL } from '@/lib/constants' 2 | import OpenAI from 'openai' 3 | const openai = new OpenAI() 4 | 5 | export async function POST(request: Request) { 6 | const { messages } = await request.json() 7 | 8 | console.log('Incoming messages', messages) 9 | 10 | try { 11 | const response = await openai.chat.completions.create({ 12 | model: MODEL, 13 | // System prompt is already included in the messages array 14 | messages 15 | }) 16 | 17 | const result = response.choices[0].message 18 | return new Response(JSON.stringify(result)) 19 | } catch (error: any) { 20 | console.error('Error in POST handler:', error) 21 | return new Response(JSON.stringify({ error: error.message }), { 22 | status: 500 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /challenge1/frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge1/frontend/app/favicon.ico -------------------------------------------------------------------------------- /challenge1/frontend/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge1/frontend/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /challenge1/frontend/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge1/frontend/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /challenge1/frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: var(--font-geist-sans); 21 | } 22 | 23 | .font-mono, pre, code { 24 | font-family: var(--font-geist-mono) !important; 25 | } 26 | 27 | @layer utilities { 28 | .text-balance { 29 | text-wrap: balance; 30 | } 31 | } 32 | 33 | @layer base { 34 | :root { 35 | --background: 0 0% 100%; 36 | --foreground: 20 14.3% 4.1%; 37 | --card: 0 0% 100%; 38 | --card-foreground: 20 14.3% 4.1%; 39 | --popover: 0 0% 100%; 40 | --popover-foreground: 20 14.3% 4.1%; 41 | --primary: 24 9.8% 10%; 42 | --primary-foreground: 60 9.1% 97.8%; 43 | --secondary: 60 4.8% 95.9%; 44 | --secondary-foreground: 24 9.8% 10%; 45 | --muted: 60 4.8% 95.9%; 46 | --muted-foreground: 25 5.3% 44.7%; 47 | --accent: 60 4.8% 95.9%; 48 | --accent-foreground: 24 9.8% 10%; 49 | --destructive: 0 84.2% 60.2%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 20 5.9% 90%; 52 | --input: 20 5.9% 90%; 53 | --ring: 20 14.3% 4.1%; 54 | --chart-1: 12 76% 61%; 55 | --chart-2: 173 58% 39%; 56 | --chart-3: 197 37% 24%; 57 | --chart-4: 43 74% 66%; 58 | --chart-5: 27 87% 67%; 59 | --radius: 0.5rem; 60 | } 61 | .dark { 62 | --background: 20 14.3% 4.1%; 63 | --foreground: 60 9.1% 97.8%; 64 | --card: 20 14.3% 4.1%; 65 | --card-foreground: 60 9.1% 97.8%; 66 | --popover: 20 14.3% 4.1%; 67 | --popover-foreground: 60 9.1% 97.8%; 68 | --primary: 60 9.1% 97.8%; 69 | --primary-foreground: 24 9.8% 10%; 70 | --secondary: 12 6.5% 15.1%; 71 | --secondary-foreground: 60 9.1% 97.8%; 72 | --muted: 12 6.5% 15.1%; 73 | --muted-foreground: 24 5.4% 63.9%; 74 | --accent: 12 6.5% 15.1%; 75 | --accent-foreground: 60 9.1% 97.8%; 76 | --destructive: 0 62.8% 30.6%; 77 | --destructive-foreground: 60 9.1% 97.8%; 78 | --border: 12 6.5% 15.1%; 79 | --input: 12 6.5% 15.1%; 80 | --ring: 24 5.7% 82.9%; 81 | --chart-1: 220 70% 50%; 82 | --chart-2: 160 60% 45%; 83 | --chart-3: 30 80% 55%; 84 | --chart-4: 280 65% 60%; 85 | --chart-5: 340 75% 55%; 86 | } 87 | } 88 | 89 | @layer base { 90 | * { 91 | @apply border-border; 92 | } 93 | body { 94 | @apply bg-background text-foreground; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /challenge1/frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import localFont from 'next/font/local' 3 | import './globals.css' 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900' 9 | }) 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900' 14 | }) 15 | 16 | export const metadata: Metadata = { 17 | title: 'Wanderlust Concierge', 18 | description: 'Builder Lab tutorial', 19 | icons: { 20 | icon: '/imgs/convex_icon.svg' 21 | } 22 | } 23 | 24 | export default function RootLayout({ 25 | children 26 | }: Readonly<{ 27 | children: React.ReactNode 28 | }>) { 29 | return ( 30 | 31 | 32 |
33 |
{children}
34 |
35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /challenge1/frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Assistant from '@/components/assistant' 2 | 3 | export default function Main() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /challenge1/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /challenge1/frontend/components/assistant.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState } from 'react' 3 | import Chat from './chat' 4 | import useConversationStore from '@/stores/useConversationStore' 5 | import { handleTurn, Item } from '@/lib/assistant' 6 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 7 | 8 | const Assistant: React.FC = () => { 9 | const [loading, setLoading] = useState(false) 10 | const { chatMessages, addConversationItem, addChatMessage } = 11 | useConversationStore() 12 | 13 | const handleSendMessage = async (message: string) => { 14 | if (!message.trim()) return 15 | 16 | const userItem: Item = { 17 | type: 'message', 18 | role: 'user', 19 | content: message.trim() 20 | } 21 | const userMessage: ChatCompletionMessageParam = { 22 | role: 'user', 23 | content: message.trim() 24 | } 25 | 26 | try { 27 | setLoading(true) 28 | addConversationItem(userMessage) 29 | addChatMessage(userItem) 30 | 31 | await handleTurn() 32 | } catch (error) { 33 | console.error('Error processing message:', error) 34 | } finally { 35 | setLoading(false) 36 | } 37 | } 38 | 39 | return ( 40 |
41 | 46 |
47 | ) 48 | } 49 | 50 | export default Assistant 51 | -------------------------------------------------------------------------------- /challenge1/frontend/components/message.css: -------------------------------------------------------------------------------- 1 | @keyframes bounce { 2 | 0%, 3 | 80%, 4 | 100% { 5 | transform: scale(0); 6 | } 7 | 40% { 8 | transform: scale(1); 9 | } 10 | } 11 | 12 | .dot { 13 | width: 5px; 14 | height: 5px; 15 | margin: 0 5px; 16 | border-radius: 50%; 17 | display: inline-block; 18 | animation: bounce 1.4s infinite ease-in-out both; 19 | background-color: white; 20 | } 21 | 22 | .dot:nth-child(1) { 23 | animation-delay: -0.32s; 24 | } 25 | 26 | .dot:nth-child(2) { 27 | animation-delay: -0.16s; 28 | } 29 | -------------------------------------------------------------------------------- /challenge1/frontend/components/message.tsx: -------------------------------------------------------------------------------- 1 | import { MessageItem } from '@/lib/assistant' 2 | import React from 'react' 3 | import ReactMarkdown from 'react-markdown' 4 | import './message.css' 5 | 6 | interface MessageProps { 7 | message: MessageItem 8 | loading?: boolean 9 | } 10 | 11 | const Message: React.FC = ({ message, loading }) => { 12 | return ( 13 |
14 | {message.role === 'user' ? ( 15 |
16 |
17 |
18 | Me 19 |
20 |
21 |
22 |
23 | {message.content as string} 24 |
25 |
26 |
27 |
28 |
29 | ) : ( 30 |
31 |
32 | Assistant 33 |
34 |
35 |
36 |
37 | {loading ? ( 38 |
39 | 40 | 41 | 42 |
43 | ) : ( 44 | {message.content as string} 45 | )} 46 |
47 |
48 |
49 |
50 | )} 51 |
52 | ) 53 | } 54 | 55 | export default Message 56 | -------------------------------------------------------------------------------- /challenge1/frontend/components/tool-call.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { FunctionCallItem, Item } from '@/lib/assistant' 4 | import { ChevronRight, Code, LoaderCircle, X, Zap } from 'lucide-react' 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 6 | import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism' 7 | 8 | interface FunctionCallProps { 9 | functionCall: FunctionCallItem 10 | previousItem: Item 11 | } 12 | 13 | const ToolCall: React.FC = ({ 14 | functionCall, 15 | previousItem 16 | }: FunctionCallProps) => { 17 | const [showDetails, setShowDetails] = React.useState(false) 18 | const toggleShowDetails = () => { 19 | setShowDetails(!showDetails) 20 | } 21 | 22 | return ( 23 |
24 |
25 |
26 | {previousItem.type === 'function_call' ? ( 27 |
28 |
29 |
30 | ) : null} 31 | 32 |
33 |
34 |
35 | 36 | 37 | {functionCall.name 38 | .split('_') 39 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 40 | .join(' ')} 41 | 42 |
43 | 49 | 50 | 51 |
52 | {showDetails && ( 53 |
54 |
55 | 69 | {JSON.stringify(functionCall.parsedArguments, null, 2)} 70 | 71 |
72 |
73 | {functionCall.output ? ( 74 | 87 | {JSON.stringify(JSON.parse(functionCall.output), null, 2)} 88 | 89 | ) : null} 90 |
91 |
92 | )} 93 |
94 |
95 |
96 |
97 | ) 98 | } 99 | 100 | export default ToolCall 101 | -------------------------------------------------------------------------------- /challenge1/frontend/lib/assistant.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 2 | import { SYSTEM_PROMPT } from './constants' 3 | import useConversationStore from '@/stores/useConversationStore' 4 | 5 | export interface MessageItem { 6 | type: 'message' 7 | role: 'user' | 'assistant' | 'system' 8 | content: string 9 | } 10 | 11 | export interface FunctionCallItem { 12 | type: 'function_call' 13 | status: 'in_progress' | 'completed' | 'failed' 14 | id: string 15 | name: string 16 | arguments: string 17 | parsedArguments: any 18 | output: string | null 19 | } 20 | 21 | export type Item = MessageItem | FunctionCallItem 22 | 23 | export const handleTurn = async () => { 24 | const { 25 | chatMessages, 26 | conversationItems, 27 | setChatMessages, 28 | setConversationItems 29 | } = useConversationStore.getState() 30 | 31 | const allConversationItems: ChatCompletionMessageParam[] = [ 32 | { 33 | role: 'system', 34 | content: SYSTEM_PROMPT 35 | }, 36 | ...conversationItems 37 | ] 38 | 39 | try { 40 | // To use the python backend, replace by 41 | //const response = await fetch('http://localhost:8000/get_response', { 42 | const response = await fetch('/api/get_response', { 43 | method: 'POST', 44 | headers: { 45 | 'Content-Type': 'application/json' 46 | }, 47 | body: JSON.stringify({ messages: allConversationItems }) 48 | }) 49 | 50 | if (!response.ok) { 51 | console.error(`Error: ${response.statusText}`) 52 | return 53 | } 54 | 55 | const data: MessageItem = await response.json() 56 | 57 | console.log('Response', data) 58 | 59 | // Update chat messages 60 | chatMessages.push(data) 61 | setChatMessages([...chatMessages]) 62 | 63 | // Update conversation items 64 | conversationItems.push(data) 65 | setConversationItems([...conversationItems]) 66 | } catch (error) { 67 | console.error('Error processing messages:', error) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /challenge1/frontend/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MODEL = 'gpt-4o' 2 | 3 | // System prompt for the assistant 4 | export const SYSTEM_PROMPT = ` 5 | You are the Wanderlust Concierge, an AI travel assistant helping users plan their trips. 6 | When users ask for your help, prompt them to understand where they would like to travel and for how long. 7 | You can also make suggestions for destinations, activities, and accommodations. 8 | Make relevant suggestions based on the user's preferences. 9 | ` 10 | // Initial message that will be displayed in the chat 11 | export const INITIAL_MESSAGE = ` 12 | Hi, how can I help you for your upcoming trip? 13 | ` 14 | -------------------------------------------------------------------------------- /challenge1/frontend/lib/tools.ts: -------------------------------------------------------------------------------- 1 | export const handleTool = async (toolName: string, parameters: any) => {} 2 | 3 | export const tools = [] 4 | -------------------------------------------------------------------------------- /challenge1/frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /challenge1/frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /challenge1/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@npmcli/fs": "^4.0.0", 13 | "@reach/visually-hidden": "^0.18.0", 14 | "@xyflow/react": "^12.3.0", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.441.0", 18 | "next": "14.2.11", 19 | "openai": "^4.61.0", 20 | "partial-json": "^0.1.7", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "react-markdown": "^9.0.1", 24 | "react-syntax-highlighter": "^15.5.0", 25 | "recharts": "^2.12.7", 26 | "tailwind-merge": "^2.5.2", 27 | "tailwindcss-animate": "^1.0.7", 28 | "vaul": "^1.0.0", 29 | "zod": "^3.23.8", 30 | "zustand": "^5.0.2" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20.16.10", 34 | "@types/react": "^18.3.10", 35 | "@types/react-dom": "^18", 36 | "@types/react-syntax-highlighter": "^15.5.13", 37 | "eslint": "^9.13.0", 38 | "eslint-config-next": "^15.0.1", 39 | "postcss": "^8", 40 | "tailwindcss": "^3.4.1", 41 | "typescript": "^5.6.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /challenge1/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /challenge1/frontend/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: 'lf', 4 | semi: false, 5 | useTabs: false, 6 | singleQuote: true, 7 | arrowParens: 'avoid', 8 | tabWidth: 2, 9 | trailingComma: 'none', 10 | importOrder: [ 11 | '^(react/(.*)$)|^(react$)', 12 | '^(next/(.*)$)|^(next$)', 13 | '', 14 | '', 15 | '^types$', 16 | '^@/types/(.*)$', 17 | '^@/config/(.*)$', 18 | '^@/lib/(.*)$', 19 | '^@/hooks/(.*)$', 20 | '^@/components/ui/(.*)$', 21 | '^@/components/(.*)$', 22 | '^@/registry/(.*)$', 23 | '^@/styles/(.*)$', 24 | '^@/app/(.*)$', 25 | '', 26 | '^[./]' 27 | ], 28 | importOrderSeparation: false, 29 | importOrderSortSpecifiers: true, 30 | importOrderBuiltinModulesToTop: true, 31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], 32 | importOrderMergeDuplicateImports: true, 33 | importOrderCombineTypeAndValueImports: true 34 | } 35 | -------------------------------------------------------------------------------- /challenge1/frontend/public/imgs/convex_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /challenge1/frontend/stores/useConversationStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { Item } from '@/lib/assistant' 3 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 4 | import { INITIAL_MESSAGE } from '@/lib/constants' 5 | 6 | interface ConversationState { 7 | // Items displayed in the chat 8 | chatMessages: Item[] 9 | // Items sent to the Chat Completions API 10 | conversationItems: ChatCompletionMessageParam[] 11 | 12 | setChatMessages: (items: Item[]) => void 13 | setConversationItems: (messages: ChatCompletionMessageParam[]) => void 14 | addChatMessage: (item: Item) => void 15 | addConversationItem: (message: ChatCompletionMessageParam) => void 16 | } 17 | 18 | const useConversationStore = create((set, get) => ({ 19 | chatMessages: [ 20 | { 21 | type: 'message', 22 | role: 'assistant', 23 | content: INITIAL_MESSAGE 24 | } 25 | ], 26 | conversationItems: [], 27 | setChatMessages: items => set({ chatMessages: items }), 28 | setConversationItems: messages => set({ conversationItems: messages }), 29 | addChatMessage: item => 30 | set(state => ({ chatMessages: [...state.chatMessages, item] })), 31 | addConversationItem: message => 32 | set(state => ({ conversationItems: [...state.conversationItems, message] })) 33 | })) 34 | 35 | export default useConversationStore 36 | -------------------------------------------------------------------------------- /challenge1/frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './app/**/*.{js,ts,jsx,tsx,mdx}', 9 | './config/**/*.{js,ts,jsx,tsx,mdx}' 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | } 54 | }, 55 | borderRadius: { 56 | lg: 'var(--radius)', 57 | md: 'calc(var(--radius) - 2px)', 58 | sm: 'calc(var(--radius) - 4px)' 59 | } 60 | } 61 | }, 62 | plugins: [require('tailwindcss-animate')] 63 | // safelist: [ 64 | // 'mb-8', 65 | // 'bg-gray-300', 66 | // 'bg-gray-200', 67 | // 'w-48', 68 | // 'h-32', 69 | // 'text-gray-700', 70 | // 'text-gray-800', 71 | // 'px-1.5', 72 | // 'py-0.5', 73 | // 'mr-2', 74 | // 'gap-y-4' 75 | // ] 76 | } 77 | export default config 78 | -------------------------------------------------------------------------------- /challenge1/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /challenge1/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starting_point", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /challenge1/python-backend/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /challenge1/python-backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | instance/ 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | 9 | # Virtual environments 10 | .venv/ 11 | venv/ -------------------------------------------------------------------------------- /challenge1/python-backend/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from openai import OpenAI 3 | from dotenv import load_dotenv 4 | from flask_cors import CORS 5 | load_dotenv() 6 | 7 | client = OpenAI() 8 | 9 | app = Flask(__name__) 10 | CORS(app) 11 | 12 | MODEL = "gpt-4o" 13 | 14 | @app.route('/') 15 | def home(): 16 | return "Server is running" 17 | 18 | @app.route('/get_response', methods=['POST']) 19 | def get_response(): 20 | data = request.get_json() 21 | messages = data['messages'] 22 | print("Incoming messages", messages) 23 | completion = client.chat.completions.create( 24 | model=MODEL, 25 | # System prompt is already included in the messages array 26 | messages=messages 27 | ) 28 | response_message = completion.choices[0].message 29 | return jsonify(response_message) 30 | 31 | if __name__ == '__main__': 32 | # Debug mode should be set to False in production 33 | app.run(debug=True, port=8000) 34 | -------------------------------------------------------------------------------- /challenge1/python-backend/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.8.0 3 | blinker==1.9.0 4 | certifi==2024.12.14 5 | click==8.1.8 6 | distro==1.9.0 7 | Flask==3.1.0 8 | h11==0.14.0 9 | httpcore==1.0.7 10 | httpx==0.28.1 11 | idna==3.10 12 | itsdangerous==2.2.0 13 | Jinja2==3.1.5 14 | jiter==0.8.2 15 | MarkupSafe==3.0.2 16 | openai==1.60.0 17 | pydantic==2.10.5 18 | pydantic_core==2.27.2 19 | sniffio==1.3.1 20 | tqdm==4.67.1 21 | typing_extensions==4.12.2 22 | Werkzeug==3.1.3 23 | python-dotenv==1.0.1 24 | Flask-Cors==5.0.0 -------------------------------------------------------------------------------- /challenge2/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /challenge2/frontend/.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /challenge2/frontend/app/api/get_response/route.ts: -------------------------------------------------------------------------------- 1 | import { MODEL } from '@/lib/constants' 2 | import { tools } from '@/lib/tools' 3 | import OpenAI from 'openai' 4 | import { ChatCompletionTool } from 'openai/resources/chat/completions.mjs' 5 | const openai = new OpenAI() 6 | 7 | export async function POST(request: Request) { 8 | const { messages } = await request.json() 9 | 10 | console.log('Incoming messages', messages) 11 | 12 | try { 13 | const response = await openai.chat.completions.create({ 14 | model: MODEL, 15 | // System prompt is already included in the messages array 16 | messages, 17 | tools: tools as ChatCompletionTool[] 18 | }) 19 | 20 | const result = response.choices[0].message 21 | return new Response(JSON.stringify(result)) 22 | } catch (error: any) { 23 | console.error('Error in POST handler:', error) 24 | return new Response(JSON.stringify({ error: error.message }), { 25 | status: 500 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /challenge2/frontend/app/api/search_location/route.ts: -------------------------------------------------------------------------------- 1 | import { getJson } from 'serpapi' 2 | 3 | export async function POST(request: Request) { 4 | const { location, search_query } = await request.json() 5 | 6 | console.log('Search location', location, search_query) 7 | 8 | try { 9 | const serpApiKey = process.env.SERPAPI_API_KEY 10 | if (!serpApiKey) { 11 | throw new Error('SERPAPI_API_KEY is not defined') 12 | } 13 | 14 | // Search results using SerpAPI 15 | const response = await getJson({ 16 | engine: 'google', 17 | q: search_query, 18 | location: location, 19 | api_key: serpApiKey, 20 | limit: 5 21 | }) 22 | 23 | const result = response.organic_results 24 | 25 | console.log('Response', result) 26 | return new Response(JSON.stringify(result)) 27 | } catch (error: any) { 28 | console.error('Error in POST handler:', error) 29 | return new Response(JSON.stringify({ error: error.message }), { 30 | status: 500 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /challenge2/frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge2/frontend/app/favicon.ico -------------------------------------------------------------------------------- /challenge2/frontend/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge2/frontend/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /challenge2/frontend/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge2/frontend/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /challenge2/frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: var(--font-geist-sans); 21 | } 22 | 23 | .font-mono, pre, code { 24 | font-family: var(--font-geist-mono) !important; 25 | } 26 | 27 | @layer utilities { 28 | .text-balance { 29 | text-wrap: balance; 30 | } 31 | } 32 | 33 | @layer base { 34 | :root { 35 | --background: 0 0% 100%; 36 | --foreground: 20 14.3% 4.1%; 37 | --card: 0 0% 100%; 38 | --card-foreground: 20 14.3% 4.1%; 39 | --popover: 0 0% 100%; 40 | --popover-foreground: 20 14.3% 4.1%; 41 | --primary: 24 9.8% 10%; 42 | --primary-foreground: 60 9.1% 97.8%; 43 | --secondary: 60 4.8% 95.9%; 44 | --secondary-foreground: 24 9.8% 10%; 45 | --muted: 60 4.8% 95.9%; 46 | --muted-foreground: 25 5.3% 44.7%; 47 | --accent: 60 4.8% 95.9%; 48 | --accent-foreground: 24 9.8% 10%; 49 | --destructive: 0 84.2% 60.2%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 20 5.9% 90%; 52 | --input: 20 5.9% 90%; 53 | --ring: 20 14.3% 4.1%; 54 | --chart-1: 12 76% 61%; 55 | --chart-2: 173 58% 39%; 56 | --chart-3: 197 37% 24%; 57 | --chart-4: 43 74% 66%; 58 | --chart-5: 27 87% 67%; 59 | --radius: 0.5rem; 60 | } 61 | .dark { 62 | --background: 20 14.3% 4.1%; 63 | --foreground: 60 9.1% 97.8%; 64 | --card: 20 14.3% 4.1%; 65 | --card-foreground: 60 9.1% 97.8%; 66 | --popover: 20 14.3% 4.1%; 67 | --popover-foreground: 60 9.1% 97.8%; 68 | --primary: 60 9.1% 97.8%; 69 | --primary-foreground: 24 9.8% 10%; 70 | --secondary: 12 6.5% 15.1%; 71 | --secondary-foreground: 60 9.1% 97.8%; 72 | --muted: 12 6.5% 15.1%; 73 | --muted-foreground: 24 5.4% 63.9%; 74 | --accent: 12 6.5% 15.1%; 75 | --accent-foreground: 60 9.1% 97.8%; 76 | --destructive: 0 62.8% 30.6%; 77 | --destructive-foreground: 60 9.1% 97.8%; 78 | --border: 12 6.5% 15.1%; 79 | --input: 12 6.5% 15.1%; 80 | --ring: 24 5.7% 82.9%; 81 | --chart-1: 220 70% 50%; 82 | --chart-2: 160 60% 45%; 83 | --chart-3: 30 80% 55%; 84 | --chart-4: 280 65% 60%; 85 | --chart-5: 340 75% 55%; 86 | } 87 | } 88 | 89 | @layer base { 90 | * { 91 | @apply border-border; 92 | } 93 | body { 94 | @apply bg-background text-foreground; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /challenge2/frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import localFont from 'next/font/local' 3 | import './globals.css' 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900' 9 | }) 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900' 14 | }) 15 | 16 | export const metadata: Metadata = { 17 | title: 'Wanderlust Concierge', 18 | description: 'Builder Lab tutorial', 19 | icons: { 20 | icon: '/imgs/convex_icon.svg' 21 | } 22 | } 23 | 24 | export default function RootLayout({ 25 | children 26 | }: Readonly<{ 27 | children: React.ReactNode 28 | }>) { 29 | return ( 30 | 31 | 32 |
33 |
{children}
34 |
35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /challenge2/frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Assistant from '@/components/assistant' 2 | 3 | export default function Main() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /challenge2/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /challenge2/frontend/components/assistant.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState } from 'react' 3 | import Chat from './chat' 4 | import useConversationStore from '@/stores/useConversationStore' 5 | import { handleTurn, Item } from '@/lib/assistant' 6 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 7 | 8 | const Assistant: React.FC = () => { 9 | const [loading, setLoading] = useState(false) 10 | const { chatMessages, addConversationItem, addChatMessage } = 11 | useConversationStore() 12 | 13 | const handleSendMessage = async (message: string) => { 14 | if (!message.trim()) return 15 | 16 | const userItem: Item = { 17 | type: 'message', 18 | role: 'user', 19 | content: message.trim() 20 | } 21 | const userMessage: ChatCompletionMessageParam = { 22 | role: 'user', 23 | content: message.trim() 24 | } 25 | 26 | try { 27 | setLoading(true) 28 | addConversationItem(userMessage) 29 | addChatMessage(userItem) 30 | 31 | await handleTurn() 32 | } catch (error) { 33 | console.error('Error processing message:', error) 34 | } finally { 35 | setLoading(false) 36 | } 37 | } 38 | 39 | return ( 40 |
41 | 46 |
47 | ) 48 | } 49 | 50 | export default Assistant 51 | -------------------------------------------------------------------------------- /challenge2/frontend/components/message.css: -------------------------------------------------------------------------------- 1 | @keyframes bounce { 2 | 0%, 3 | 80%, 4 | 100% { 5 | transform: scale(0); 6 | } 7 | 40% { 8 | transform: scale(1); 9 | } 10 | } 11 | 12 | .dot { 13 | width: 5px; 14 | height: 5px; 15 | margin: 0 5px; 16 | border-radius: 50%; 17 | display: inline-block; 18 | animation: bounce 1.4s infinite ease-in-out both; 19 | background-color: white; 20 | } 21 | 22 | .dot:nth-child(1) { 23 | animation-delay: -0.32s; 24 | } 25 | 26 | .dot:nth-child(2) { 27 | animation-delay: -0.16s; 28 | } 29 | -------------------------------------------------------------------------------- /challenge2/frontend/components/message.tsx: -------------------------------------------------------------------------------- 1 | import { MessageItem } from '@/lib/assistant' 2 | import React from 'react' 3 | import ReactMarkdown from 'react-markdown' 4 | import './message.css' 5 | 6 | interface MessageProps { 7 | message: MessageItem 8 | loading?: boolean 9 | } 10 | 11 | const Message: React.FC = ({ message, loading }) => { 12 | return ( 13 |
14 | {message.role === 'user' ? ( 15 |
16 |
17 |
18 | Me 19 |
20 |
21 |
22 |
23 | {message.content as string} 24 |
25 |
26 |
27 |
28 |
29 | ) : ( 30 |
31 |
32 | Assistant 33 |
34 |
35 |
36 |
37 | {loading ? ( 38 |
39 | 40 | 41 | 42 |
43 | ) : ( 44 | {message.content as string} 45 | )} 46 |
47 |
48 |
49 |
50 | )} 51 |
52 | ) 53 | } 54 | 55 | export default Message 56 | -------------------------------------------------------------------------------- /challenge2/frontend/components/tool-call.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { FunctionCallItem, Item } from '@/lib/assistant' 4 | import { ChevronRight, Code, LoaderCircle, X, Zap } from 'lucide-react' 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 6 | import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism' 7 | 8 | interface FunctionCallProps { 9 | functionCall: FunctionCallItem 10 | previousItem: Item 11 | } 12 | 13 | const ToolCall: React.FC = ({ 14 | functionCall, 15 | previousItem 16 | }: FunctionCallProps) => { 17 | const [showDetails, setShowDetails] = React.useState(false) 18 | const toggleShowDetails = () => { 19 | setShowDetails(!showDetails) 20 | } 21 | 22 | return ( 23 |
24 |
25 |
26 | {previousItem.type === 'function_call' ? ( 27 |
28 |
29 |
30 | ) : null} 31 | 32 |
33 |
34 |
35 | 36 | 37 | {functionCall.name 38 | .split('_') 39 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 40 | .join(' ')} 41 | 42 |
43 | 49 | 50 | 51 |
52 | {showDetails && ( 53 |
54 |
55 | 69 | {JSON.stringify(functionCall.parsedArguments, null, 2)} 70 | 71 |
72 |
73 | {functionCall.output ? ( 74 | 87 | {JSON.stringify(JSON.parse(functionCall.output), null, 2)} 88 | 89 | ) : null} 90 |
91 |
92 | )} 93 |
94 |
95 |
96 |
97 | ) 98 | } 99 | 100 | export default ToolCall 101 | -------------------------------------------------------------------------------- /challenge2/frontend/lib/assistant.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 2 | import { SYSTEM_PROMPT } from './constants' 3 | import useConversationStore from '@/stores/useConversationStore' 4 | import { handleTool } from './tools' 5 | 6 | export interface MessageItem { 7 | type: 'message' 8 | role: 'user' | 'assistant' | 'system' 9 | content: string 10 | } 11 | 12 | export interface FunctionCallItem { 13 | type: 'function_call' 14 | status: 'in_progress' | 'completed' | 'failed' 15 | id: string 16 | name: string 17 | arguments: string 18 | parsedArguments: any 19 | output: string | null 20 | } 21 | 22 | export type Item = MessageItem | FunctionCallItem 23 | 24 | export const handleTurn = async () => { 25 | const { 26 | chatMessages, 27 | conversationItems, 28 | setChatMessages, 29 | setConversationItems 30 | } = useConversationStore.getState() 31 | 32 | const allConversationItems: ChatCompletionMessageParam[] = [ 33 | { 34 | role: 'system', 35 | content: SYSTEM_PROMPT 36 | }, 37 | ...conversationItems 38 | ] 39 | 40 | try { 41 | // To use the python backend, replace by 42 | //const response = await fetch('http://localhost:8000/get_response', { 43 | const response = await fetch('/api/get_response', { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json' 47 | }, 48 | body: JSON.stringify({ messages: allConversationItems }) 49 | }) 50 | 51 | if (!response.ok) { 52 | console.error(`Error: ${response.statusText}`) 53 | return 54 | } 55 | 56 | const data: MessageItem = await response.json() 57 | 58 | // Update conversation items 59 | conversationItems.push(data) 60 | setConversationItems([...conversationItems]) 61 | 62 | const lastMessage = conversationItems[conversationItems.length - 1] 63 | if ( 64 | lastMessage && 65 | lastMessage.role === 'assistant' && 66 | lastMessage.tool_calls && 67 | lastMessage.tool_calls.length > 0 68 | ) { 69 | // Get tool call result 70 | const toolCallResult = await handleTool( 71 | lastMessage.tool_calls[0].function.name, 72 | lastMessage.tool_calls[0].function.arguments 73 | ) 74 | 75 | conversationItems.push({ 76 | role: 'tool', 77 | tool_call_id: lastMessage.tool_calls[0].id, 78 | content: JSON.stringify(toolCallResult) 79 | }) 80 | 81 | setConversationItems([...conversationItems]) 82 | await handleTurn() 83 | } else { 84 | // Update chat messages 85 | chatMessages.push(data) 86 | setChatMessages([...chatMessages]) 87 | } 88 | } catch (error) { 89 | console.error('Error processing messages:', error) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /challenge2/frontend/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MODEL = 'gpt-4o' 2 | 3 | // System prompt for the assistant 4 | export const SYSTEM_PROMPT = ` 5 | You are the Wanderlust Concierge, an AI travel assistant helping users plan their trips. 6 | When users ask for your help, prompt them to understand where they would like to travel and for how long. 7 | You can also make suggestions for destinations, activities, and accommodations. 8 | When prompted by the user or appropriate in the conversation, you can use the search_location tool to search for landmarks or hotels where the user would like to visit. 9 | ` 10 | // Initial message that will be displayed in the chat 11 | export const INITIAL_MESSAGE = ` 12 | Hi, how can I help you for your upcoming trip? 13 | ` 14 | -------------------------------------------------------------------------------- /challenge2/frontend/lib/tools.ts: -------------------------------------------------------------------------------- 1 | export const handleTool = async (toolName: string, parameters: any) => { 2 | if (toolName === 'search_location') { 3 | console.log('Handling tool search_location', parameters) 4 | const { location, search_query } = JSON.parse(parameters) 5 | // If using the python backend, use the following endpoint: 6 | //const response = await fetch('http://localhost:8000/search_location', { 7 | const response = await fetch('/api/search_location', { 8 | method: 'POST', 9 | body: JSON.stringify({ location, search_query }), 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | } 13 | }) 14 | const data = await response.json() 15 | return data 16 | } 17 | } 18 | 19 | export const tools = [ 20 | { 21 | type: 'function', 22 | function: { 23 | name: 'search_location', 24 | description: 'Search for a landmark or hotel at a given location', 25 | parameters: { 26 | type: 'object', 27 | properties: { 28 | location: { 29 | type: 'string', 30 | description: 31 | 'The location to search in, in format City, (State), Country' 32 | }, 33 | search_query: { 34 | type: 'string', 35 | description: 36 | 'The query to search for, for example "boutique hotels" or "must-see landmarks"' 37 | } 38 | }, 39 | required: ['location', 'search_query'], 40 | additionalProperties: false 41 | }, 42 | strict: true 43 | } 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /challenge2/frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /challenge2/frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /challenge2/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@npmcli/fs": "^4.0.0", 13 | "@reach/visually-hidden": "^0.18.0", 14 | "@xyflow/react": "^12.3.0", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.441.0", 18 | "next": "14.2.11", 19 | "openai": "^4.61.0", 20 | "partial-json": "^0.1.7", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "react-markdown": "^9.0.1", 24 | "react-syntax-highlighter": "^15.5.0", 25 | "recharts": "^2.12.7", 26 | "serpapi": "^2.1.0", 27 | "tailwind-merge": "^2.5.2", 28 | "tailwindcss-animate": "^1.0.7", 29 | "vaul": "^1.0.0", 30 | "zod": "^3.23.8", 31 | "zustand": "^5.0.2" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^20.16.10", 35 | "@types/react": "^18.3.10", 36 | "@types/react-dom": "^18", 37 | "@types/react-syntax-highlighter": "^15.5.13", 38 | "eslint": "^9.13.0", 39 | "eslint-config-next": "^15.0.1", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.1", 42 | "typescript": "^5.6.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /challenge2/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /challenge2/frontend/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: 'lf', 4 | semi: false, 5 | useTabs: false, 6 | singleQuote: true, 7 | arrowParens: 'avoid', 8 | tabWidth: 2, 9 | trailingComma: 'none', 10 | importOrder: [ 11 | '^(react/(.*)$)|^(react$)', 12 | '^(next/(.*)$)|^(next$)', 13 | '', 14 | '', 15 | '^types$', 16 | '^@/types/(.*)$', 17 | '^@/config/(.*)$', 18 | '^@/lib/(.*)$', 19 | '^@/hooks/(.*)$', 20 | '^@/components/ui/(.*)$', 21 | '^@/components/(.*)$', 22 | '^@/registry/(.*)$', 23 | '^@/styles/(.*)$', 24 | '^@/app/(.*)$', 25 | '', 26 | '^[./]' 27 | ], 28 | importOrderSeparation: false, 29 | importOrderSortSpecifiers: true, 30 | importOrderBuiltinModulesToTop: true, 31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], 32 | importOrderMergeDuplicateImports: true, 33 | importOrderCombineTypeAndValueImports: true 34 | } 35 | -------------------------------------------------------------------------------- /challenge2/frontend/public/imgs/convex_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /challenge2/frontend/stores/useConversationStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { Item } from '@/lib/assistant' 3 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 4 | import { INITIAL_MESSAGE } from '@/lib/constants' 5 | 6 | interface ConversationState { 7 | // Items displayed in the chat 8 | chatMessages: Item[] 9 | // Items sent to the Chat Completions API 10 | conversationItems: ChatCompletionMessageParam[] 11 | 12 | setChatMessages: (items: Item[]) => void 13 | setConversationItems: (messages: ChatCompletionMessageParam[]) => void 14 | addChatMessage: (item: Item) => void 15 | addConversationItem: (message: ChatCompletionMessageParam) => void 16 | } 17 | 18 | const useConversationStore = create((set, get) => ({ 19 | chatMessages: [ 20 | { 21 | type: 'message', 22 | role: 'assistant', 23 | content: INITIAL_MESSAGE 24 | } 25 | ], 26 | conversationItems: [], 27 | setChatMessages: items => set({ chatMessages: items }), 28 | setConversationItems: messages => set({ conversationItems: messages }), 29 | addChatMessage: item => 30 | set(state => ({ chatMessages: [...state.chatMessages, item] })), 31 | addConversationItem: message => 32 | set(state => ({ conversationItems: [...state.conversationItems, message] })) 33 | })) 34 | 35 | export default useConversationStore 36 | -------------------------------------------------------------------------------- /challenge2/frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './app/**/*.{js,ts,jsx,tsx,mdx}', 9 | './config/**/*.{js,ts,jsx,tsx,mdx}' 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | } 54 | }, 55 | borderRadius: { 56 | lg: 'var(--radius)', 57 | md: 'calc(var(--radius) - 2px)', 58 | sm: 'calc(var(--radius) - 4px)' 59 | } 60 | } 61 | }, 62 | plugins: [require('tailwindcss-animate')] 63 | // safelist: [ 64 | // 'mb-8', 65 | // 'bg-gray-300', 66 | // 'bg-gray-200', 67 | // 'w-48', 68 | // 'h-32', 69 | // 'text-gray-700', 70 | // 'text-gray-800', 71 | // 'px-1.5', 72 | // 'py-0.5', 73 | // 'mr-2', 74 | // 'gap-y-4' 75 | // ] 76 | } 77 | export default config 78 | -------------------------------------------------------------------------------- /challenge2/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /challenge2/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starting_point", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /challenge2/python-backend/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /challenge2/python-backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | instance/ 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | 9 | # Virtual environments 10 | .venv/ 11 | venv/ -------------------------------------------------------------------------------- /challenge2/python-backend/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from openai import OpenAI 3 | from dotenv import load_dotenv 4 | from flask_cors import CORS 5 | import os 6 | from serpapi import GoogleSearch 7 | load_dotenv() 8 | 9 | client = OpenAI() 10 | 11 | app = Flask(__name__) 12 | CORS(app) 13 | 14 | MODEL = "gpt-4o" 15 | 16 | tools = [ 17 | { 18 | "type": "function", 19 | "function": { 20 | "name": "search_location", 21 | "description": "Search for a landmark or hotel at a given location", 22 | "parameters": { 23 | "type": "object", 24 | "properties": { 25 | "location": { 26 | "type": "string", 27 | "description": 28 | "The location to search in, in format City, (State), Country" 29 | }, 30 | "search_query": { 31 | "type": "string", 32 | "description": 33 | "The query to search for, for example 'boutique hotels' or 'must-see landmarks'" 34 | } 35 | }, 36 | "required": ["location", "search_query"], 37 | "additionalProperties": False 38 | }, 39 | "strict": True 40 | } 41 | } 42 | ] 43 | 44 | @app.route('/') 45 | def home(): 46 | return "Server is running" 47 | 48 | @app.route('/get_response', methods=['POST']) 49 | def get_response(): 50 | data = request.get_json() 51 | messages = data['messages'] 52 | print("Incoming messages", messages) 53 | completion = client.chat.completions.create( 54 | model=MODEL, 55 | # System prompt is already included in the messages array 56 | messages=messages, 57 | tools=tools 58 | ) 59 | response_message = completion.choices[0].message 60 | return jsonify(response_message) 61 | 62 | @app.route('/search_location', methods=['POST']) 63 | def search_location(): 64 | data = request.get_json() 65 | location = data['location'] 66 | search_query = data['search_query'] 67 | 68 | serpApiKey = os.getenv('SERPAPI_API_KEY') 69 | if not serpApiKey: 70 | raise ValueError('SERPAPI_API_KEY is not defined') 71 | 72 | params = { 73 | "engine": "google", 74 | "q": search_query, 75 | "location": location, 76 | "api_key": serpApiKey, 77 | "limit": 5 78 | } 79 | 80 | search = GoogleSearch(params) 81 | results = search.get_dict() 82 | organic_results = results["organic_results"] 83 | return organic_results 84 | 85 | if __name__ == '__main__': 86 | # Debug mode should be set to False in production 87 | app.run(debug=True, port=8000) 88 | -------------------------------------------------------------------------------- /challenge2/python-backend/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.8.0 3 | blinker==1.9.0 4 | certifi==2024.12.14 5 | charset-normalizer==3.4.1 6 | click==8.1.8 7 | distro==1.9.0 8 | Flask==3.1.0 9 | Flask-Cors==5.0.0 10 | google_search_results==2.4.2 11 | h11==0.14.0 12 | httpcore==1.0.7 13 | httpx==0.28.1 14 | idna==3.10 15 | itsdangerous==2.2.0 16 | Jinja2==3.1.5 17 | jiter==0.8.2 18 | MarkupSafe==3.0.2 19 | openai==1.60.0 20 | pydantic==2.10.5 21 | pydantic_core==2.27.2 22 | python-dotenv==1.0.1 23 | requests==2.32.3 24 | sniffio==1.3.1 25 | tqdm==4.67.1 26 | typing_extensions==4.12.2 27 | urllib3==2.3.0 28 | Werkzeug==3.1.3 29 | -------------------------------------------------------------------------------- /challenge3/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /challenge3/frontend/.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /challenge3/frontend/app/api/create_itinerary/route.ts: -------------------------------------------------------------------------------- 1 | import { ITINERARY_PROMPT } from '@/lib/constants' 2 | import OpenAI from 'openai' 3 | const openai = new OpenAI() 4 | 5 | export async function POST(request: Request) { 6 | const { stops } = await request.json() 7 | 8 | console.log('Planning itinerary', stops) 9 | 10 | try { 11 | const response = await openai.chat.completions.create({ 12 | model: 'o1', 13 | messages: [ 14 | { role: 'system', content: ITINERARY_PROMPT }, 15 | { role: 'user', content: JSON.stringify(stops) } 16 | ] 17 | }) 18 | 19 | const result = response.choices[0].message.content 20 | return new Response(JSON.stringify({ itinerary: result })) 21 | } catch (error: any) { 22 | console.error('Error in POST handler:', error) 23 | return new Response(JSON.stringify({ error: error.message }), { 24 | status: 500 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /challenge3/frontend/app/api/get_response/route.ts: -------------------------------------------------------------------------------- 1 | import { MODEL } from '@/lib/constants' 2 | import { tools } from '@/lib/tools' 3 | import OpenAI from 'openai' 4 | import { ChatCompletionTool } from 'openai/resources/chat/completions.mjs' 5 | const openai = new OpenAI() 6 | 7 | export async function POST(request: Request) { 8 | const { messages } = await request.json() 9 | 10 | console.log('Incoming messages', messages) 11 | 12 | try { 13 | const response = await openai.chat.completions.create({ 14 | model: MODEL, 15 | // System prompt is already included in the messages array 16 | messages, 17 | tools: tools as ChatCompletionTool[] 18 | }) 19 | 20 | const result = response.choices[0].message 21 | return new Response(JSON.stringify(result)) 22 | } catch (error: any) { 23 | console.error('Error in POST handler:', error) 24 | return new Response(JSON.stringify({ error: error.message }), { 25 | status: 500 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /challenge3/frontend/app/api/search_location/route.ts: -------------------------------------------------------------------------------- 1 | import { getJson } from 'serpapi' 2 | 3 | export async function POST(request: Request) { 4 | const { location, search_query } = await request.json() 5 | 6 | console.log('Search location', location, search_query) 7 | 8 | try { 9 | const serpApiKey = process.env.SERPAPI_API_KEY 10 | if (!serpApiKey) { 11 | throw new Error('SERPAPI_API_KEY is not defined') 12 | } 13 | 14 | // Search results using SerpAPI 15 | const response = await getJson({ 16 | engine: 'google', 17 | q: search_query, 18 | location: location, 19 | api_key: serpApiKey, 20 | limit: 5 21 | }) 22 | 23 | const result = response.organic_results 24 | 25 | console.log('Response', result) 26 | return new Response(JSON.stringify(result)) 27 | } catch (error: any) { 28 | console.error('Error in POST handler:', error) 29 | return new Response(JSON.stringify({ error: error.message }), { 30 | status: 500 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /challenge3/frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge3/frontend/app/favicon.ico -------------------------------------------------------------------------------- /challenge3/frontend/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge3/frontend/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /challenge3/frontend/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge3/frontend/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /challenge3/frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: var(--font-geist-sans); 21 | } 22 | 23 | .font-mono, pre, code { 24 | font-family: var(--font-geist-mono) !important; 25 | } 26 | 27 | @layer utilities { 28 | .text-balance { 29 | text-wrap: balance; 30 | } 31 | } 32 | 33 | @layer base { 34 | :root { 35 | --background: 0 0% 100%; 36 | --foreground: 20 14.3% 4.1%; 37 | --card: 0 0% 100%; 38 | --card-foreground: 20 14.3% 4.1%; 39 | --popover: 0 0% 100%; 40 | --popover-foreground: 20 14.3% 4.1%; 41 | --primary: 24 9.8% 10%; 42 | --primary-foreground: 60 9.1% 97.8%; 43 | --secondary: 60 4.8% 95.9%; 44 | --secondary-foreground: 24 9.8% 10%; 45 | --muted: 60 4.8% 95.9%; 46 | --muted-foreground: 25 5.3% 44.7%; 47 | --accent: 60 4.8% 95.9%; 48 | --accent-foreground: 24 9.8% 10%; 49 | --destructive: 0 84.2% 60.2%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 20 5.9% 90%; 52 | --input: 20 5.9% 90%; 53 | --ring: 20 14.3% 4.1%; 54 | --chart-1: 12 76% 61%; 55 | --chart-2: 173 58% 39%; 56 | --chart-3: 197 37% 24%; 57 | --chart-4: 43 74% 66%; 58 | --chart-5: 27 87% 67%; 59 | --radius: 0.5rem; 60 | } 61 | .dark { 62 | --background: 20 14.3% 4.1%; 63 | --foreground: 60 9.1% 97.8%; 64 | --card: 20 14.3% 4.1%; 65 | --card-foreground: 60 9.1% 97.8%; 66 | --popover: 20 14.3% 4.1%; 67 | --popover-foreground: 60 9.1% 97.8%; 68 | --primary: 60 9.1% 97.8%; 69 | --primary-foreground: 24 9.8% 10%; 70 | --secondary: 12 6.5% 15.1%; 71 | --secondary-foreground: 60 9.1% 97.8%; 72 | --muted: 12 6.5% 15.1%; 73 | --muted-foreground: 24 5.4% 63.9%; 74 | --accent: 12 6.5% 15.1%; 75 | --accent-foreground: 60 9.1% 97.8%; 76 | --destructive: 0 62.8% 30.6%; 77 | --destructive-foreground: 60 9.1% 97.8%; 78 | --border: 12 6.5% 15.1%; 79 | --input: 12 6.5% 15.1%; 80 | --ring: 24 5.7% 82.9%; 81 | --chart-1: 220 70% 50%; 82 | --chart-2: 160 60% 45%; 83 | --chart-3: 30 80% 55%; 84 | --chart-4: 280 65% 60%; 85 | --chart-5: 340 75% 55%; 86 | } 87 | } 88 | 89 | @layer base { 90 | * { 91 | @apply border-border; 92 | } 93 | body { 94 | @apply bg-background text-foreground; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /challenge3/frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import localFont from 'next/font/local' 3 | import './globals.css' 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900' 9 | }) 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900' 14 | }) 15 | 16 | export const metadata: Metadata = { 17 | title: 'Wanderlust Concierge', 18 | description: 'Builder Lab tutorial', 19 | icons: { 20 | icon: '/imgs/convex_icon.svg' 21 | } 22 | } 23 | 24 | export default function RootLayout({ 25 | children 26 | }: Readonly<{ 27 | children: React.ReactNode 28 | }>) { 29 | return ( 30 | 31 | 32 |
33 |
{children}
34 |
35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /challenge3/frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Assistant from '@/components/assistant' 2 | 3 | export default function Main() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /challenge3/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /challenge3/frontend/components/assistant.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState } from 'react' 3 | import Chat from './chat' 4 | import useConversationStore from '@/stores/useConversationStore' 5 | import { handleTurn, Item } from '@/lib/assistant' 6 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 7 | 8 | const Assistant: React.FC = () => { 9 | const [loading, setLoading] = useState(false) 10 | const { chatMessages, addConversationItem, addChatMessage } = 11 | useConversationStore() 12 | 13 | const handleSendMessage = async (message: string) => { 14 | if (!message.trim()) return 15 | 16 | const userItem: Item = { 17 | type: 'message', 18 | role: 'user', 19 | content: message.trim() 20 | } 21 | const userMessage: ChatCompletionMessageParam = { 22 | role: 'user', 23 | content: message.trim() 24 | } 25 | 26 | try { 27 | setLoading(true) 28 | addConversationItem(userMessage) 29 | addChatMessage(userItem) 30 | 31 | await handleTurn() 32 | } catch (error) { 33 | console.error('Error processing message:', error) 34 | } finally { 35 | setLoading(false) 36 | } 37 | } 38 | 39 | return ( 40 |
41 | 46 |
47 | ) 48 | } 49 | 50 | export default Assistant 51 | -------------------------------------------------------------------------------- /challenge3/frontend/components/message.css: -------------------------------------------------------------------------------- 1 | @keyframes bounce { 2 | 0%, 3 | 80%, 4 | 100% { 5 | transform: scale(0); 6 | } 7 | 40% { 8 | transform: scale(1); 9 | } 10 | } 11 | 12 | .dot { 13 | width: 5px; 14 | height: 5px; 15 | margin: 0 5px; 16 | border-radius: 50%; 17 | display: inline-block; 18 | animation: bounce 1.4s infinite ease-in-out both; 19 | background-color: white; 20 | } 21 | 22 | .dot:nth-child(1) { 23 | animation-delay: -0.32s; 24 | } 25 | 26 | .dot:nth-child(2) { 27 | animation-delay: -0.16s; 28 | } 29 | -------------------------------------------------------------------------------- /challenge3/frontend/components/message.tsx: -------------------------------------------------------------------------------- 1 | import { MessageItem } from '@/lib/assistant' 2 | import React from 'react' 3 | import ReactMarkdown from 'react-markdown' 4 | import './message.css' 5 | 6 | interface MessageProps { 7 | message: MessageItem 8 | loading?: boolean 9 | } 10 | 11 | const Message: React.FC = ({ message, loading }) => { 12 | return ( 13 |
14 | {message.role === 'user' ? ( 15 |
16 |
17 |
18 | Me 19 |
20 |
21 |
22 |
23 | {message.content as string} 24 |
25 |
26 |
27 |
28 |
29 | ) : ( 30 |
31 |
32 | Assistant 33 |
34 |
35 |
36 |
37 | {loading ? ( 38 |
39 | 40 | 41 | 42 |
43 | ) : ( 44 | {message.content as string} 45 | )} 46 |
47 |
48 |
49 |
50 | )} 51 |
52 | ) 53 | } 54 | 55 | export default Message 56 | -------------------------------------------------------------------------------- /challenge3/frontend/components/tool-call.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { FunctionCallItem, Item } from '@/lib/assistant' 4 | import { ChevronRight, Code, LoaderCircle, X, Zap } from 'lucide-react' 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 6 | import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism' 7 | 8 | interface FunctionCallProps { 9 | functionCall: FunctionCallItem 10 | previousItem: Item 11 | } 12 | 13 | const ToolCall: React.FC = ({ 14 | functionCall, 15 | previousItem 16 | }: FunctionCallProps) => { 17 | const [showDetails, setShowDetails] = React.useState(false) 18 | const toggleShowDetails = () => { 19 | setShowDetails(!showDetails) 20 | } 21 | 22 | return ( 23 |
24 |
25 |
26 | {previousItem.type === 'function_call' ? ( 27 |
28 |
29 |
30 | ) : null} 31 | 32 |
33 |
34 |
35 | 36 | 37 | {functionCall.name 38 | .split('_') 39 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 40 | .join(' ')} 41 | 42 |
43 | 49 | 50 | 51 |
52 | {showDetails && ( 53 |
54 |
55 | 69 | {JSON.stringify(functionCall.parsedArguments, null, 2)} 70 | 71 |
72 |
73 | {functionCall.output ? ( 74 | 87 | {JSON.stringify(JSON.parse(functionCall.output), null, 2)} 88 | 89 | ) : null} 90 |
91 |
92 | )} 93 |
94 |
95 |
96 |
97 | ) 98 | } 99 | 100 | export default ToolCall 101 | -------------------------------------------------------------------------------- /challenge3/frontend/lib/assistant.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 2 | import { SYSTEM_PROMPT } from './constants' 3 | import useConversationStore from '@/stores/useConversationStore' 4 | import { handleTool } from './tools' 5 | 6 | export interface MessageItem { 7 | type: 'message' 8 | role: 'user' | 'assistant' | 'system' 9 | content: string 10 | } 11 | 12 | export interface FunctionCallItem { 13 | type: 'function_call' 14 | status: 'in_progress' | 'completed' | 'failed' 15 | id: string 16 | name: string 17 | arguments: string 18 | parsedArguments: any 19 | output: string | null 20 | } 21 | 22 | export type Item = MessageItem | FunctionCallItem 23 | 24 | export const handleTurn = async () => { 25 | const { 26 | chatMessages, 27 | conversationItems, 28 | setChatMessages, 29 | setConversationItems 30 | } = useConversationStore.getState() 31 | 32 | const allConversationItems: ChatCompletionMessageParam[] = [ 33 | { 34 | role: 'system', 35 | content: SYSTEM_PROMPT 36 | }, 37 | ...conversationItems 38 | ] 39 | 40 | try { 41 | // To use the python backend, replace by 42 | //const response = await fetch('http://localhost:8000/get_response', { 43 | const response = await fetch('/api/get_response', { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json' 47 | }, 48 | body: JSON.stringify({ messages: allConversationItems }) 49 | }) 50 | 51 | if (!response.ok) { 52 | console.error(`Error: ${response.statusText}`) 53 | return 54 | } 55 | 56 | const data: MessageItem = await response.json() 57 | 58 | // Update conversation items 59 | conversationItems.push(data) 60 | setConversationItems([...conversationItems]) 61 | 62 | const lastMessage = conversationItems[conversationItems.length - 1] 63 | if ( 64 | lastMessage && 65 | lastMessage.role === 'assistant' && 66 | lastMessage.tool_calls && 67 | lastMessage.tool_calls.length > 0 68 | ) { 69 | // Get tool call result 70 | const toolCallResult = await handleTool( 71 | lastMessage.tool_calls[0].function.name, 72 | lastMessage.tool_calls[0].function.arguments 73 | ) 74 | 75 | conversationItems.push({ 76 | role: 'tool', 77 | tool_call_id: lastMessage.tool_calls[0].id, 78 | content: JSON.stringify(toolCallResult) 79 | }) 80 | 81 | setConversationItems([...conversationItems]) 82 | await handleTurn() 83 | } else { 84 | // Update chat messages 85 | chatMessages.push(data) 86 | setChatMessages([...chatMessages]) 87 | } 88 | } catch (error) { 89 | console.error('Error processing messages:', error) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /challenge3/frontend/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MODEL = 'gpt-4o' 2 | 3 | // System prompt for the assistant 4 | export const SYSTEM_PROMPT = ` 5 | You are the Wanderlust Concierge, an AI travel assistant helping users plan their trips. 6 | When users ask for your help, prompt them to understand where they would like to travel and for how long. 7 | You can also make suggestions for destinations, activities, and accommodations. 8 | When prompted by the user or appropriate in the conversation, you can use the search_location tool to search for landmarks or hotels where the user would like to visit. 9 | ` 10 | // Initial message that will be displayed in the chat 11 | export const INITIAL_MESSAGE = ` 12 | Hi, how can I help you for your upcoming trip? 13 | ` 14 | 15 | // System prompt for reasoning tool call 16 | export const ITINERARY_PROMPT = ` 17 | You will be provided with a list of travel stops. 18 | You will need to plan an itinerary for the user based on the stops. 19 | Take into account the duration of the stay at each stop, the type of visit, and the number of participants. 20 | Suggest the best order in which to visit the stops based on how convenient it is to travel between them. 21 | Propose the optimal order of stops, and feel free to suggest slight deviations for the number of days, 22 | including when to leave for the next stop (morning, evening...) to make the most of the trip. 23 | ` 24 | -------------------------------------------------------------------------------- /challenge3/frontend/lib/tools.ts: -------------------------------------------------------------------------------- 1 | export const handleTool = async (toolName: string, parameters: any) => { 2 | if (toolName === 'search_location') { 3 | console.log('Handling tool search_location', parameters) 4 | const { location, search_query } = JSON.parse(parameters) 5 | // If using the python backend, use the following endpoint: 6 | //const response = await fetch('http://localhost:8000/search_location', { 7 | const response = await fetch('/api/search_location', { 8 | method: 'POST', 9 | body: JSON.stringify({ location, search_query }), 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | } 13 | }) 14 | const data = await response.json() 15 | return data 16 | } 17 | if (toolName === 'plan_itinerary') { 18 | console.log('Handling tool plan_itinerary', parameters) 19 | const { stops } = JSON.parse(parameters) 20 | // If using the python backend, use the following endpoint: 21 | //const response = await fetch('http://localhost:8000/plan_itinerary', { 22 | const response = await fetch('/api/create_itinerary', { 23 | method: 'POST', 24 | body: JSON.stringify({ stops }), 25 | headers: { 'Content-Type': 'application/json' } 26 | }) 27 | const data = await response.json() 28 | return data 29 | } 30 | } 31 | 32 | export const tools = [ 33 | { 34 | type: 'function', 35 | function: { 36 | name: 'search_location', 37 | description: 'Search for a landmark or hotel at a given location', 38 | parameters: { 39 | type: 'object', 40 | properties: { 41 | location: { 42 | type: 'string', 43 | description: 44 | 'The location to search in, in format City, (State), Country' 45 | }, 46 | search_query: { 47 | type: 'string', 48 | description: 49 | 'The query to search for, for example "boutique hotels" or "must-see landmarks"' 50 | } 51 | }, 52 | required: ['location', 'search_query'], 53 | additionalProperties: false 54 | }, 55 | strict: true 56 | } 57 | }, 58 | { 59 | type: 'function', 60 | function: { 61 | name: 'plan_itinerary', 62 | description: 'Plan an itinerary with the given parameters', 63 | parameters: { 64 | type: 'object', 65 | properties: { 66 | stops: { 67 | type: 'array', 68 | description: 'Travel stops', 69 | items: { 70 | type: 'object', 71 | properties: { 72 | location: { 73 | type: 'string' 74 | }, 75 | duration: { 76 | type: 'number', 77 | description: 'Duration in days' 78 | }, 79 | type: { 80 | type: 'string', 81 | description: 82 | 'Type of visit: sightseeing, shopping, snorkeling, etc.' 83 | }, 84 | participants: { 85 | type: 'number', 86 | description: 'Number of participants' 87 | } 88 | }, 89 | required: ['location', 'duration', 'type', 'participants'] 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | ] 97 | -------------------------------------------------------------------------------- /challenge3/frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /challenge3/frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /challenge3/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@npmcli/fs": "^4.0.0", 13 | "@reach/visually-hidden": "^0.18.0", 14 | "@xyflow/react": "^12.3.0", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.441.0", 18 | "next": "14.2.11", 19 | "openai": "^4.61.0", 20 | "partial-json": "^0.1.7", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "react-markdown": "^9.0.1", 24 | "react-syntax-highlighter": "^15.5.0", 25 | "recharts": "^2.12.7", 26 | "serpapi": "^2.1.0", 27 | "tailwind-merge": "^2.5.2", 28 | "tailwindcss-animate": "^1.0.7", 29 | "vaul": "^1.0.0", 30 | "zod": "^3.23.8", 31 | "zustand": "^5.0.2" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^20.16.10", 35 | "@types/react": "^18.3.10", 36 | "@types/react-dom": "^18", 37 | "@types/react-syntax-highlighter": "^15.5.13", 38 | "eslint": "^9.13.0", 39 | "eslint-config-next": "^15.0.1", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.1", 42 | "typescript": "^5.6.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /challenge3/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /challenge3/frontend/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: 'lf', 4 | semi: false, 5 | useTabs: false, 6 | singleQuote: true, 7 | arrowParens: 'avoid', 8 | tabWidth: 2, 9 | trailingComma: 'none', 10 | importOrder: [ 11 | '^(react/(.*)$)|^(react$)', 12 | '^(next/(.*)$)|^(next$)', 13 | '', 14 | '', 15 | '^types$', 16 | '^@/types/(.*)$', 17 | '^@/config/(.*)$', 18 | '^@/lib/(.*)$', 19 | '^@/hooks/(.*)$', 20 | '^@/components/ui/(.*)$', 21 | '^@/components/(.*)$', 22 | '^@/registry/(.*)$', 23 | '^@/styles/(.*)$', 24 | '^@/app/(.*)$', 25 | '', 26 | '^[./]' 27 | ], 28 | importOrderSeparation: false, 29 | importOrderSortSpecifiers: true, 30 | importOrderBuiltinModulesToTop: true, 31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], 32 | importOrderMergeDuplicateImports: true, 33 | importOrderCombineTypeAndValueImports: true 34 | } 35 | -------------------------------------------------------------------------------- /challenge3/frontend/public/imgs/convex_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /challenge3/frontend/stores/useConversationStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { Item } from '@/lib/assistant' 3 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 4 | import { INITIAL_MESSAGE } from '@/lib/constants' 5 | 6 | interface ConversationState { 7 | // Items displayed in the chat 8 | chatMessages: Item[] 9 | // Items sent to the Chat Completions API 10 | conversationItems: ChatCompletionMessageParam[] 11 | 12 | setChatMessages: (items: Item[]) => void 13 | setConversationItems: (messages: ChatCompletionMessageParam[]) => void 14 | addChatMessage: (item: Item) => void 15 | addConversationItem: (message: ChatCompletionMessageParam) => void 16 | } 17 | 18 | const useConversationStore = create((set, get) => ({ 19 | chatMessages: [ 20 | { 21 | type: 'message', 22 | role: 'assistant', 23 | content: INITIAL_MESSAGE 24 | } 25 | ], 26 | conversationItems: [], 27 | setChatMessages: items => set({ chatMessages: items }), 28 | setConversationItems: messages => set({ conversationItems: messages }), 29 | addChatMessage: item => 30 | set(state => ({ chatMessages: [...state.chatMessages, item] })), 31 | addConversationItem: message => 32 | set(state => ({ conversationItems: [...state.conversationItems, message] })) 33 | })) 34 | 35 | export default useConversationStore 36 | -------------------------------------------------------------------------------- /challenge3/frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './app/**/*.{js,ts,jsx,tsx,mdx}', 9 | './config/**/*.{js,ts,jsx,tsx,mdx}' 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | } 54 | }, 55 | borderRadius: { 56 | lg: 'var(--radius)', 57 | md: 'calc(var(--radius) - 2px)', 58 | sm: 'calc(var(--radius) - 4px)' 59 | } 60 | } 61 | }, 62 | plugins: [require('tailwindcss-animate')] 63 | // safelist: [ 64 | // 'mb-8', 65 | // 'bg-gray-300', 66 | // 'bg-gray-200', 67 | // 'w-48', 68 | // 'h-32', 69 | // 'text-gray-700', 70 | // 'text-gray-800', 71 | // 'px-1.5', 72 | // 'py-0.5', 73 | // 'mr-2', 74 | // 'gap-y-4' 75 | // ] 76 | } 77 | export default config 78 | -------------------------------------------------------------------------------- /challenge3/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /challenge3/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starting_point", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /challenge3/python-backend/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /challenge3/python-backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | instance/ 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | 9 | # Virtual environments 10 | .venv/ 11 | venv/ -------------------------------------------------------------------------------- /challenge3/python-backend/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.8.0 3 | blinker==1.9.0 4 | certifi==2024.12.14 5 | charset-normalizer==3.4.1 6 | click==8.1.8 7 | distro==1.9.0 8 | Flask==3.1.0 9 | Flask-Cors==5.0.0 10 | google_search_results==2.4.2 11 | h11==0.14.0 12 | httpcore==1.0.7 13 | httpx==0.28.1 14 | idna==3.10 15 | itsdangerous==2.2.0 16 | Jinja2==3.1.5 17 | jiter==0.8.2 18 | MarkupSafe==3.0.2 19 | openai==1.60.0 20 | pydantic==2.10.5 21 | pydantic_core==2.27.2 22 | python-dotenv==1.0.1 23 | requests==2.32.3 24 | sniffio==1.3.1 25 | tqdm==4.67.1 26 | typing_extensions==4.12.2 27 | urllib3==2.3.0 28 | Werkzeug==3.1.3 29 | -------------------------------------------------------------------------------- /challenge4/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /challenge4/frontend/.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /challenge4/frontend/app/api/create_itinerary/route.ts: -------------------------------------------------------------------------------- 1 | import { ITINERARY_PROMPT } from '@/lib/constants' 2 | import OpenAI from 'openai' 3 | const openai = new OpenAI() 4 | 5 | export async function POST(request: Request) { 6 | const { stops } = await request.json() 7 | 8 | console.log('Planning itinerary', stops) 9 | 10 | try { 11 | const response = await openai.chat.completions.create({ 12 | model: 'o1', 13 | messages: [ 14 | { role: 'system', content: ITINERARY_PROMPT }, 15 | { role: 'user', content: JSON.stringify(stops) } 16 | ] 17 | }) 18 | 19 | const result = response.choices[0].message.content 20 | return new Response(JSON.stringify({ itinerary: result })) 21 | } catch (error: any) { 22 | console.error('Error in POST handler:', error) 23 | return new Response(JSON.stringify({ error: error.message }), { 24 | status: 500 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /challenge4/frontend/app/api/get_response/route.ts: -------------------------------------------------------------------------------- 1 | import { MODEL } from '@/lib/constants' 2 | import { tools } from '@/lib/tools' 3 | import OpenAI from 'openai' 4 | import { ChatCompletionTool } from 'openai/resources/chat/completions.mjs' 5 | const openai = new OpenAI() 6 | 7 | export async function POST(request: Request) { 8 | const { messages } = await request.json() 9 | 10 | console.log('Incoming messages', messages) 11 | 12 | try { 13 | const response = await openai.chat.completions.create({ 14 | model: MODEL, 15 | // System prompt is already included in the messages array 16 | messages, 17 | tools: tools as ChatCompletionTool[] 18 | }) 19 | 20 | const result = response.choices[0].message 21 | return new Response(JSON.stringify(result)) 22 | } catch (error: any) { 23 | console.error('Error in POST handler:', error) 24 | return new Response(JSON.stringify({ error: error.message }), { 25 | status: 500 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /challenge4/frontend/app/api/search_location/route.ts: -------------------------------------------------------------------------------- 1 | import { getJson } from 'serpapi' 2 | 3 | export async function POST(request: Request) { 4 | const { location, search_query } = await request.json() 5 | 6 | console.log('Search location', location, search_query) 7 | 8 | try { 9 | const serpApiKey = process.env.SERPAPI_API_KEY 10 | if (!serpApiKey) { 11 | throw new Error('SERPAPI_API_KEY is not defined') 12 | } 13 | 14 | // Search results using SerpAPI 15 | const response = await getJson({ 16 | engine: 'google', 17 | q: search_query, 18 | location: location, 19 | api_key: serpApiKey, 20 | limit: 5 21 | }) 22 | 23 | const result = response.organic_results 24 | 25 | console.log('Response', result) 26 | return new Response(JSON.stringify(result)) 27 | } catch (error: any) { 28 | console.error('Error in POST handler:', error) 29 | return new Response(JSON.stringify({ error: error.message }), { 30 | status: 500 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /challenge4/frontend/app/api/session/route.ts: -------------------------------------------------------------------------------- 1 | import { REALTIME_MODEL, VOICE } from '@/lib/constants' 2 | 3 | // Get an ephemeral session token from the /realtime/sessions endpoint 4 | export async function GET() { 5 | try { 6 | const r = await fetch('https://api.openai.com/v1/realtime/sessions', { 7 | method: 'POST', 8 | headers: { 9 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 10 | 'Content-Type': 'application/json' 11 | }, 12 | body: JSON.stringify({ 13 | model: REALTIME_MODEL, 14 | voice: VOICE 15 | }) 16 | }) 17 | 18 | return new Response(r.body, { 19 | status: 200, 20 | headers: { 21 | 'Content-Type': 'application/json' 22 | } 23 | }) 24 | } catch (error: any) { 25 | console.error('Error:', error) 26 | return new Response(JSON.stringify({ error: error.message }), { 27 | status: 500 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /challenge4/frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge4/frontend/app/favicon.ico -------------------------------------------------------------------------------- /challenge4/frontend/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge4/frontend/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /challenge4/frontend/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge4/frontend/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /challenge4/frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: var(--font-geist-sans); 21 | } 22 | 23 | .font-mono, pre, code { 24 | font-family: var(--font-geist-mono) !important; 25 | } 26 | 27 | @layer utilities { 28 | .text-balance { 29 | text-wrap: balance; 30 | } 31 | } 32 | 33 | @layer base { 34 | :root { 35 | --background: 0 0% 100%; 36 | --foreground: 20 14.3% 4.1%; 37 | --card: 0 0% 100%; 38 | --card-foreground: 20 14.3% 4.1%; 39 | --popover: 0 0% 100%; 40 | --popover-foreground: 20 14.3% 4.1%; 41 | --primary: 24 9.8% 10%; 42 | --primary-foreground: 60 9.1% 97.8%; 43 | --secondary: 60 4.8% 95.9%; 44 | --secondary-foreground: 24 9.8% 10%; 45 | --muted: 60 4.8% 95.9%; 46 | --muted-foreground: 25 5.3% 44.7%; 47 | --accent: 60 4.8% 95.9%; 48 | --accent-foreground: 24 9.8% 10%; 49 | --destructive: 0 84.2% 60.2%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 20 5.9% 90%; 52 | --input: 20 5.9% 90%; 53 | --ring: 20 14.3% 4.1%; 54 | --chart-1: 12 76% 61%; 55 | --chart-2: 173 58% 39%; 56 | --chart-3: 197 37% 24%; 57 | --chart-4: 43 74% 66%; 58 | --chart-5: 27 87% 67%; 59 | --radius: 0.5rem; 60 | } 61 | .dark { 62 | --background: 20 14.3% 4.1%; 63 | --foreground: 60 9.1% 97.8%; 64 | --card: 20 14.3% 4.1%; 65 | --card-foreground: 60 9.1% 97.8%; 66 | --popover: 20 14.3% 4.1%; 67 | --popover-foreground: 60 9.1% 97.8%; 68 | --primary: 60 9.1% 97.8%; 69 | --primary-foreground: 24 9.8% 10%; 70 | --secondary: 12 6.5% 15.1%; 71 | --secondary-foreground: 60 9.1% 97.8%; 72 | --muted: 12 6.5% 15.1%; 73 | --muted-foreground: 24 5.4% 63.9%; 74 | --accent: 12 6.5% 15.1%; 75 | --accent-foreground: 60 9.1% 97.8%; 76 | --destructive: 0 62.8% 30.6%; 77 | --destructive-foreground: 60 9.1% 97.8%; 78 | --border: 12 6.5% 15.1%; 79 | --input: 12 6.5% 15.1%; 80 | --ring: 24 5.7% 82.9%; 81 | --chart-1: 220 70% 50%; 82 | --chart-2: 160 60% 45%; 83 | --chart-3: 30 80% 55%; 84 | --chart-4: 280 65% 60%; 85 | --chart-5: 340 75% 55%; 86 | } 87 | } 88 | 89 | @layer base { 90 | * { 91 | @apply border-border; 92 | } 93 | body { 94 | @apply bg-background text-foreground; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /challenge4/frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import localFont from 'next/font/local' 3 | import './globals.css' 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900' 9 | }) 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900' 14 | }) 15 | 16 | export const metadata: Metadata = { 17 | title: 'Wanderlust Concierge', 18 | description: 'Builder Lab tutorial', 19 | icons: { 20 | icon: '/imgs/convex_icon.svg' 21 | } 22 | } 23 | 24 | export default function RootLayout({ 25 | children 26 | }: Readonly<{ 27 | children: React.ReactNode 28 | }>) { 29 | return ( 30 | 31 | 32 |
33 |
{children}
34 |
35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /challenge4/frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Assistant from '@/components/assistant' 2 | 3 | export default function Main() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /challenge4/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /challenge4/frontend/components/assistant.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState } from 'react' 3 | import Chat from './chat' 4 | import useConversationStore from '@/stores/useConversationStore' 5 | import { handleTurn, Item } from '@/lib/assistant' 6 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 7 | 8 | const Assistant: React.FC = () => { 9 | const [loading, setLoading] = useState(false) 10 | const { chatMessages, addConversationItem, addChatMessage } = 11 | useConversationStore() 12 | 13 | const handleSendMessage = async (message: string) => { 14 | if (!message.trim()) return 15 | 16 | const userItem: Item = { 17 | type: 'message', 18 | role: 'user', 19 | content: message.trim() 20 | } 21 | const userMessage: ChatCompletionMessageParam = { 22 | role: 'user', 23 | content: message.trim() 24 | } 25 | 26 | try { 27 | setLoading(true) 28 | addConversationItem(userMessage) 29 | addChatMessage(userItem) 30 | 31 | await handleTurn() 32 | } catch (error) { 33 | console.error('Error processing message:', error) 34 | } finally { 35 | setLoading(false) 36 | } 37 | } 38 | 39 | return ( 40 |
41 | 46 |
47 | ) 48 | } 49 | 50 | export default Assistant 51 | -------------------------------------------------------------------------------- /challenge4/frontend/components/message.css: -------------------------------------------------------------------------------- 1 | @keyframes bounce { 2 | 0%, 3 | 80%, 4 | 100% { 5 | transform: scale(0); 6 | } 7 | 40% { 8 | transform: scale(1); 9 | } 10 | } 11 | 12 | .dot { 13 | width: 5px; 14 | height: 5px; 15 | margin: 0 5px; 16 | border-radius: 50%; 17 | display: inline-block; 18 | animation: bounce 1.4s infinite ease-in-out both; 19 | background-color: white; 20 | } 21 | 22 | .dot:nth-child(1) { 23 | animation-delay: -0.32s; 24 | } 25 | 26 | .dot:nth-child(2) { 27 | animation-delay: -0.16s; 28 | } 29 | -------------------------------------------------------------------------------- /challenge4/frontend/components/message.tsx: -------------------------------------------------------------------------------- 1 | import { MessageItem } from '@/lib/assistant' 2 | import React from 'react' 3 | import ReactMarkdown from 'react-markdown' 4 | import './message.css' 5 | 6 | interface MessageProps { 7 | message: MessageItem 8 | loading?: boolean 9 | } 10 | 11 | const Message: React.FC = ({ message, loading }) => { 12 | return ( 13 |
14 | {message.role === 'user' ? ( 15 |
16 |
17 |
18 | Me 19 |
20 |
21 |
22 |
23 | {message.content as string} 24 |
25 |
26 |
27 |
28 |
29 | ) : ( 30 |
31 |
32 | Assistant 33 |
34 |
35 |
36 |
37 | {loading ? ( 38 |
39 | 40 | 41 | 42 |
43 | ) : ( 44 | {message.content as string} 45 | )} 46 |
47 |
48 |
49 |
50 | )} 51 |
52 | ) 53 | } 54 | 55 | export default Message 56 | -------------------------------------------------------------------------------- /challenge4/frontend/components/tool-call.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { FunctionCallItem, Item } from '@/lib/assistant' 4 | import { ChevronRight, Code, LoaderCircle, X, Zap } from 'lucide-react' 5 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 6 | import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism' 7 | 8 | interface FunctionCallProps { 9 | functionCall: FunctionCallItem 10 | previousItem: Item 11 | } 12 | 13 | const ToolCall: React.FC = ({ 14 | functionCall, 15 | previousItem 16 | }: FunctionCallProps) => { 17 | const [showDetails, setShowDetails] = React.useState(false) 18 | const toggleShowDetails = () => { 19 | setShowDetails(!showDetails) 20 | } 21 | 22 | return ( 23 |
24 |
25 |
26 | {previousItem.type === 'function_call' ? ( 27 |
28 |
29 |
30 | ) : null} 31 | 32 |
33 |
34 |
35 | 36 | 37 | {functionCall.name 38 | .split('_') 39 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 40 | .join(' ')} 41 | 42 |
43 | 49 | 50 | 51 |
52 | {showDetails && ( 53 |
54 |
55 | 69 | {JSON.stringify(functionCall.parsedArguments, null, 2)} 70 | 71 |
72 |
73 | {functionCall.output ? ( 74 | 87 | {JSON.stringify(JSON.parse(functionCall.output), null, 2)} 88 | 89 | ) : null} 90 |
91 |
92 | )} 93 |
94 |
95 |
96 |
97 | ) 98 | } 99 | 100 | export default ToolCall 101 | -------------------------------------------------------------------------------- /challenge4/frontend/lib/assistant.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 2 | import { SYSTEM_PROMPT } from './constants' 3 | import useConversationStore from '@/stores/useConversationStore' 4 | import { handleTool } from './tools' 5 | 6 | export interface MessageItem { 7 | type: 'message' 8 | role: 'user' | 'assistant' | 'system' 9 | content: string 10 | } 11 | 12 | export interface FunctionCallItem { 13 | type: 'function_call' 14 | status: 'in_progress' | 'completed' | 'failed' 15 | id: string 16 | name: string 17 | arguments: string 18 | parsedArguments: any 19 | output: string | null 20 | } 21 | 22 | export type Item = MessageItem | FunctionCallItem 23 | 24 | export const handleTurn = async () => { 25 | const { 26 | chatMessages, 27 | conversationItems, 28 | setChatMessages, 29 | setConversationItems 30 | } = useConversationStore.getState() 31 | 32 | const allConversationItems: ChatCompletionMessageParam[] = [ 33 | { 34 | role: 'system', 35 | content: SYSTEM_PROMPT 36 | }, 37 | ...conversationItems 38 | ] 39 | 40 | try { 41 | // To use the python backend, replace by 42 | //const response = await fetch('http://localhost:8000/get_response', { 43 | const response = await fetch('/api/get_response', { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json' 47 | }, 48 | body: JSON.stringify({ messages: allConversationItems }) 49 | }) 50 | 51 | if (!response.ok) { 52 | console.error(`Error: ${response.statusText}`) 53 | return 54 | } 55 | 56 | const data: MessageItem = await response.json() 57 | 58 | // Update conversation items 59 | conversationItems.push(data) 60 | setConversationItems([...conversationItems]) 61 | 62 | const lastMessage = conversationItems[conversationItems.length - 1] 63 | if ( 64 | lastMessage && 65 | lastMessage.role === 'assistant' && 66 | lastMessage.tool_calls && 67 | lastMessage.tool_calls.length > 0 68 | ) { 69 | // Get tool call result 70 | const toolCallResult = await handleTool( 71 | lastMessage.tool_calls[0].function.name, 72 | lastMessage.tool_calls[0].function.arguments 73 | ) 74 | 75 | conversationItems.push({ 76 | role: 'tool', 77 | tool_call_id: lastMessage.tool_calls[0].id, 78 | content: JSON.stringify(toolCallResult) 79 | }) 80 | 81 | setConversationItems([...conversationItems]) 82 | await handleTurn() 83 | } else { 84 | // Update chat messages 85 | chatMessages.push(data) 86 | setChatMessages([...chatMessages]) 87 | } 88 | } catch (error) { 89 | console.error('Error processing messages:', error) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /challenge4/frontend/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MODEL = 'gpt-4o' 2 | export const REALTIME_MODEL = 'gpt-4o-realtime-preview' 3 | export const REALTIME_BASE_URL = 'https://api.openai.com/v1/realtime' 4 | export const VOICE = 'coral' 5 | 6 | // System prompt for the assistant 7 | export const SYSTEM_PROMPT = ` 8 | You are the Wanderlust Concierge, an AI travel assistant helping users plan their trips. 9 | When users ask for your help, prompt them to understand where they would like to travel and for how long. 10 | You can also make suggestions for destinations, activities, and accommodations. 11 | When prompted by the user or appropriate in the conversation, you can use the search_location tool to search for landmarks or hotels where the user would like to visit. 12 | ` 13 | // Initial message that will be displayed in the chat 14 | export const INITIAL_MESSAGE = ` 15 | Hi, how can I help you for your upcoming trip? 16 | ` 17 | 18 | // System prompt for reasoning tool call 19 | export const ITINERARY_PROMPT = ` 20 | You will be provided with a list of travel stops. 21 | You will need to plan an itinerary for the user based on the stops. 22 | Take into account the duration of the stay at each stop, the type of visit, and the number of participants. 23 | Suggest the best order in which to visit the stops based on how convenient it is to travel between them. 24 | Propose the optimal order of stops, and feel free to suggest slight deviations for the number of days, 25 | including when to leave for the next stop (morning, evening...) to make the most of the trip. 26 | ` 27 | 28 | export const REALTIME_PROMPT = ` 29 | You are the Wanderlust Concierge, a peppy AI-travel assistant helping users plan their trips. 30 | Prompt users to give you more details about their trip, and comment on how excited you are for their trip. 31 | Help them plan and organize their trip, and suggest activities and accommodations based on their preferences. 32 | ` 33 | -------------------------------------------------------------------------------- /challenge4/frontend/lib/tools.ts: -------------------------------------------------------------------------------- 1 | export const handleTool = async (toolName: string, parameters: any) => { 2 | if (toolName === 'search_location') { 3 | console.log('Handling tool search_location', parameters) 4 | const { location, search_query } = JSON.parse(parameters) 5 | // If using the python backend, use the following endpoint: 6 | //const response = await fetch('http://localhost:8000/search_location', { 7 | const response = await fetch('/api/search_location', { 8 | method: 'POST', 9 | body: JSON.stringify({ location, search_query }), 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | } 13 | }) 14 | const data = await response.json() 15 | return data 16 | } 17 | if (toolName === 'plan_itinerary') { 18 | console.log('Handling tool plan_itinerary', parameters) 19 | const { stops } = JSON.parse(parameters) 20 | // If using the python backend, use the following endpoint: 21 | //const response = await fetch('http://localhost:8000/plan_itinerary', { 22 | const response = await fetch('/api/create_itinerary', { 23 | method: 'POST', 24 | body: JSON.stringify({ stops }), 25 | headers: { 'Content-Type': 'application/json' } 26 | }) 27 | const data = await response.json() 28 | return data 29 | } 30 | } 31 | 32 | export const tools = [ 33 | { 34 | type: 'function', 35 | function: { 36 | name: 'search_location', 37 | description: 'Search for a landmark or hotel at a given location', 38 | parameters: { 39 | type: 'object', 40 | properties: { 41 | location: { 42 | type: 'string', 43 | description: 44 | 'The location to search in, in format City, (State), Country' 45 | }, 46 | search_query: { 47 | type: 'string', 48 | description: 49 | 'The query to search for, for example "boutique hotels" or "must-see landmarks"' 50 | } 51 | }, 52 | required: ['location', 'search_query'], 53 | additionalProperties: false 54 | }, 55 | strict: true 56 | } 57 | }, 58 | { 59 | type: 'function', 60 | function: { 61 | name: 'plan_itinerary', 62 | description: 'Plan an itinerary with the given parameters', 63 | parameters: { 64 | type: 'object', 65 | properties: { 66 | stops: { 67 | type: 'array', 68 | description: 'Travel stops', 69 | items: { 70 | type: 'object', 71 | properties: { 72 | location: { 73 | type: 'string' 74 | }, 75 | duration: { 76 | type: 'number', 77 | description: 'Duration in days' 78 | }, 79 | type: { 80 | type: 'string', 81 | description: 82 | 'Type of visit: sightseeing, shopping, snorkeling, etc.' 83 | }, 84 | participants: { 85 | type: 'number', 86 | description: 'Number of participants' 87 | } 88 | }, 89 | required: ['location', 'duration', 'type', 'participants'] 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | ] 97 | -------------------------------------------------------------------------------- /challenge4/frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /challenge4/frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /challenge4/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@npmcli/fs": "^4.0.0", 13 | "@reach/visually-hidden": "^0.18.0", 14 | "@xyflow/react": "^12.3.0", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.441.0", 18 | "next": "14.2.11", 19 | "openai": "^4.61.0", 20 | "partial-json": "^0.1.7", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "react-markdown": "^9.0.1", 24 | "react-syntax-highlighter": "^15.5.0", 25 | "recharts": "^2.12.7", 26 | "serpapi": "^2.1.0", 27 | "tailwind-merge": "^2.5.2", 28 | "tailwindcss-animate": "^1.0.7", 29 | "vaul": "^1.0.0", 30 | "zod": "^3.23.8", 31 | "zustand": "^5.0.2" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^20.16.10", 35 | "@types/react": "^18.3.10", 36 | "@types/react-dom": "^18", 37 | "@types/react-syntax-highlighter": "^15.5.13", 38 | "eslint": "^9.13.0", 39 | "eslint-config-next": "^15.0.1", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.1", 42 | "typescript": "^5.6.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /challenge4/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /challenge4/frontend/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: 'lf', 4 | semi: false, 5 | useTabs: false, 6 | singleQuote: true, 7 | arrowParens: 'avoid', 8 | tabWidth: 2, 9 | trailingComma: 'none', 10 | importOrder: [ 11 | '^(react/(.*)$)|^(react$)', 12 | '^(next/(.*)$)|^(next$)', 13 | '', 14 | '', 15 | '^types$', 16 | '^@/types/(.*)$', 17 | '^@/config/(.*)$', 18 | '^@/lib/(.*)$', 19 | '^@/hooks/(.*)$', 20 | '^@/components/ui/(.*)$', 21 | '^@/components/(.*)$', 22 | '^@/registry/(.*)$', 23 | '^@/styles/(.*)$', 24 | '^@/app/(.*)$', 25 | '', 26 | '^[./]' 27 | ], 28 | importOrderSeparation: false, 29 | importOrderSortSpecifiers: true, 30 | importOrderBuiltinModulesToTop: true, 31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], 32 | importOrderMergeDuplicateImports: true, 33 | importOrderCombineTypeAndValueImports: true 34 | } 35 | -------------------------------------------------------------------------------- /challenge4/frontend/public/imgs/convex_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /challenge4/frontend/stores/useConversationStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { Item } from '@/lib/assistant' 3 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 4 | import { INITIAL_MESSAGE } from '@/lib/constants' 5 | 6 | interface ConversationState { 7 | // Items displayed in the chat 8 | chatMessages: Item[] 9 | // Items sent to the Chat Completions API 10 | conversationItems: ChatCompletionMessageParam[] 11 | 12 | setChatMessages: (items: Item[]) => void 13 | setConversationItems: (messages: ChatCompletionMessageParam[]) => void 14 | addChatMessage: (item: Item) => void 15 | addConversationItem: (message: ChatCompletionMessageParam) => void 16 | } 17 | 18 | const useConversationStore = create((set, get) => ({ 19 | chatMessages: [ 20 | { 21 | type: 'message', 22 | role: 'assistant', 23 | content: INITIAL_MESSAGE 24 | } 25 | ], 26 | conversationItems: [], 27 | setChatMessages: items => set({ chatMessages: items }), 28 | setConversationItems: messages => set({ conversationItems: messages }), 29 | addChatMessage: item => 30 | set(state => ({ chatMessages: [...state.chatMessages, item] })), 31 | addConversationItem: message => 32 | set(state => ({ conversationItems: [...state.conversationItems, message] })) 33 | })) 34 | 35 | export default useConversationStore 36 | -------------------------------------------------------------------------------- /challenge4/frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './app/**/*.{js,ts,jsx,tsx,mdx}', 9 | './config/**/*.{js,ts,jsx,tsx,mdx}' 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | } 54 | }, 55 | borderRadius: { 56 | lg: 'var(--radius)', 57 | md: 'calc(var(--radius) - 2px)', 58 | sm: 'calc(var(--radius) - 4px)' 59 | } 60 | } 61 | }, 62 | plugins: [require('tailwindcss-animate')] 63 | // safelist: [ 64 | // 'mb-8', 65 | // 'bg-gray-300', 66 | // 'bg-gray-200', 67 | // 'w-48', 68 | // 'h-32', 69 | // 'text-gray-700', 70 | // 'text-gray-800', 71 | // 'px-1.5', 72 | // 'py-0.5', 73 | // 'mr-2', 74 | // 'gap-y-4' 75 | // ] 76 | } 77 | export default config 78 | -------------------------------------------------------------------------------- /challenge4/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /challenge4/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starting_point", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /challenge4/python-backend/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /challenge4/python-backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | instance/ 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | 9 | # Virtual environments 10 | .venv/ 11 | venv/ -------------------------------------------------------------------------------- /challenge4/python-backend/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.8.0 3 | blinker==1.9.0 4 | certifi==2024.12.14 5 | charset-normalizer==3.4.1 6 | click==8.1.8 7 | distro==1.9.0 8 | Flask==3.1.0 9 | Flask-Cors==5.0.0 10 | google_search_results==2.4.2 11 | h11==0.14.0 12 | httpcore==1.0.7 13 | httpx==0.28.1 14 | idna==3.10 15 | itsdangerous==2.2.0 16 | Jinja2==3.1.5 17 | jiter==0.8.2 18 | MarkupSafe==3.0.2 19 | openai==1.60.0 20 | pydantic==2.10.5 21 | pydantic_core==2.27.2 22 | python-dotenv==1.0.1 23 | requests==2.32.3 24 | sniffio==1.3.1 25 | tqdm==4.67.1 26 | typing_extensions==4.12.2 27 | urllib3==2.3.0 28 | Werkzeug==3.1.3 29 | -------------------------------------------------------------------------------- /challenge5/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /challenge5/frontend/.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /challenge5/frontend/app/api/create_itinerary/route.ts: -------------------------------------------------------------------------------- 1 | import { ITINERARY_PROMPT } from '@/lib/constants' 2 | import OpenAI from 'openai' 3 | const openai = new OpenAI() 4 | 5 | export async function POST(request: Request) { 6 | const { stops } = await request.json() 7 | 8 | console.log('Planning itinerary', stops) 9 | 10 | try { 11 | const response = await openai.chat.completions.create({ 12 | model: 'o1', 13 | messages: [ 14 | { role: 'system', content: ITINERARY_PROMPT }, 15 | { role: 'user', content: JSON.stringify(stops) } 16 | ] 17 | }) 18 | 19 | const result = response.choices[0].message.content 20 | return new Response(JSON.stringify({ itinerary: result })) 21 | } catch (error: any) { 22 | console.error('Error in POST handler:', error) 23 | return new Response(JSON.stringify({ error: error.message }), { 24 | status: 500 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /challenge5/frontend/app/api/get_response/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import { tools } from '@/lib/tools' 3 | import { ChatCompletionTool } from 'openai/resources/chat/completions.mjs' 4 | const openai = new OpenAI() 5 | 6 | const MODEL = 'gpt-4o' 7 | 8 | export async function POST(request: Request) { 9 | const { messages } = await request.json() 10 | 11 | console.log('Incoming messages', messages) 12 | 13 | try { 14 | const stream = new ReadableStream({ 15 | async start(controller) { 16 | try { 17 | console.log('Starting OpenAI stream', messages[messages.length - 1]) 18 | const openaiStream = openai.beta.chat.completions.stream({ 19 | model: MODEL, 20 | messages, 21 | tools: tools as ChatCompletionTool[] 22 | }) 23 | 24 | let functionArguments = '' 25 | let callId = '' 26 | let functionName = '' 27 | let isCollectingFunctionArgs = false 28 | 29 | for await (const part of openaiStream) { 30 | const delta = part.choices[0].delta 31 | const finishReason = part.choices[0].finish_reason 32 | 33 | if (delta.content) { 34 | const data = JSON.stringify({ 35 | event: 'assistant_delta', 36 | data: delta 37 | }) 38 | controller.enqueue(`data: ${data}\n\n`) 39 | } 40 | 41 | if (delta.tool_calls) { 42 | isCollectingFunctionArgs = true 43 | if (delta.tool_calls[0].id) { 44 | callId = delta.tool_calls[0].id 45 | } 46 | if (delta.tool_calls[0].function?.name) { 47 | functionName = delta.tool_calls[0].function.name 48 | console.log('Function execution:', functionName) 49 | } 50 | functionArguments += delta.tool_calls[0].function?.arguments || '' 51 | 52 | const data = JSON.stringify({ 53 | event: 'function_arguments_delta', 54 | data: { 55 | callId: callId, 56 | name: functionName, 57 | arguments: delta.tool_calls[0].function?.arguments 58 | } 59 | }) 60 | controller.enqueue(`data: ${data}\n\n`) 61 | } 62 | 63 | if (finishReason === 'tool_calls' && isCollectingFunctionArgs) { 64 | console.log(`tool call ${functionName} is complete`) 65 | const data = JSON.stringify({ 66 | event: 'function_arguments_done', 67 | data: { 68 | callId: callId, 69 | name: functionName, 70 | arguments: functionArguments 71 | } 72 | }) 73 | controller.enqueue(`data: ${data}\n\n`) 74 | 75 | // Reset function arguments 76 | functionArguments = '' 77 | functionName = '' 78 | isCollectingFunctionArgs = false 79 | } 80 | } 81 | 82 | controller.close() 83 | } catch (error) { 84 | console.error('Error in stream start:', error) 85 | controller.error(error) 86 | } 87 | } 88 | }) 89 | 90 | return new Response(stream, { 91 | headers: { 92 | 'Content-Type': 'text/event-stream', 93 | 'Cache-Control': 'no-cache', 94 | Connection: 'keep-alive' 95 | } 96 | }) 97 | } catch (error: any) { 98 | console.error('Error in POST handler:', error) 99 | return new Response(JSON.stringify({ error: error.message }), { 100 | status: 500 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /challenge5/frontend/app/api/search_location/route.ts: -------------------------------------------------------------------------------- 1 | import { getJson } from 'serpapi' 2 | 3 | export async function POST(request: Request) { 4 | const { location, search_query } = await request.json() 5 | 6 | console.log('Search location', location, search_query) 7 | 8 | try { 9 | const serpApiKey = process.env.SERPAPI_API_KEY 10 | if (!serpApiKey) { 11 | throw new Error('SERPAPI_API_KEY is not defined') 12 | } 13 | 14 | // Search results using SerpAPI 15 | const response = await getJson({ 16 | engine: 'google', 17 | q: search_query, 18 | location: location, 19 | api_key: serpApiKey, 20 | limit: 5 21 | }) 22 | 23 | const result = response.organic_results 24 | 25 | console.log('Response', result) 26 | return new Response(JSON.stringify(result)) 27 | } catch (error: any) { 28 | console.error('Error in POST handler:', error) 29 | return new Response(JSON.stringify({ error: error.message }), { 30 | status: 500 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /challenge5/frontend/app/api/session/route.ts: -------------------------------------------------------------------------------- 1 | import { REALTIME_MODEL, VOICE } from '@/lib/constants' 2 | 3 | // Get an ephemeral session token from the /realtime/sessions endpoint 4 | export async function GET() { 5 | try { 6 | const r = await fetch('https://api.openai.com/v1/realtime/sessions', { 7 | method: 'POST', 8 | headers: { 9 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 10 | 'Content-Type': 'application/json' 11 | }, 12 | body: JSON.stringify({ 13 | model: REALTIME_MODEL, 14 | voice: VOICE 15 | }) 16 | }) 17 | 18 | return new Response(r.body, { 19 | status: 200, 20 | headers: { 21 | 'Content-Type': 'application/json' 22 | } 23 | }) 24 | } catch (error: any) { 25 | console.error('Error:', error) 26 | return new Response(JSON.stringify({ error: error.message }), { 27 | status: 500 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /challenge5/frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge5/frontend/app/favicon.ico -------------------------------------------------------------------------------- /challenge5/frontend/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge5/frontend/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /challenge5/frontend/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openai/openai-builder-lab-solution/e87f060e4e86d599ee54bdc7b752484944278f85/challenge5/frontend/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /challenge5/frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: var(--font-geist-sans); 21 | } 22 | 23 | .font-mono, pre, code { 24 | font-family: var(--font-geist-mono) !important; 25 | } 26 | 27 | @layer utilities { 28 | .text-balance { 29 | text-wrap: balance; 30 | } 31 | } 32 | 33 | @layer base { 34 | :root { 35 | --background: 0 0% 100%; 36 | --foreground: 20 14.3% 4.1%; 37 | --card: 0 0% 100%; 38 | --card-foreground: 20 14.3% 4.1%; 39 | --popover: 0 0% 100%; 40 | --popover-foreground: 20 14.3% 4.1%; 41 | --primary: 24 9.8% 10%; 42 | --primary-foreground: 60 9.1% 97.8%; 43 | --secondary: 60 4.8% 95.9%; 44 | --secondary-foreground: 24 9.8% 10%; 45 | --muted: 60 4.8% 95.9%; 46 | --muted-foreground: 25 5.3% 44.7%; 47 | --accent: 60 4.8% 95.9%; 48 | --accent-foreground: 24 9.8% 10%; 49 | --destructive: 0 84.2% 60.2%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 20 5.9% 90%; 52 | --input: 20 5.9% 90%; 53 | --ring: 20 14.3% 4.1%; 54 | --chart-1: 12 76% 61%; 55 | --chart-2: 173 58% 39%; 56 | --chart-3: 197 37% 24%; 57 | --chart-4: 43 74% 66%; 58 | --chart-5: 27 87% 67%; 59 | --radius: 0.5rem; 60 | } 61 | .dark { 62 | --background: 20 14.3% 4.1%; 63 | --foreground: 60 9.1% 97.8%; 64 | --card: 20 14.3% 4.1%; 65 | --card-foreground: 60 9.1% 97.8%; 66 | --popover: 20 14.3% 4.1%; 67 | --popover-foreground: 60 9.1% 97.8%; 68 | --primary: 60 9.1% 97.8%; 69 | --primary-foreground: 24 9.8% 10%; 70 | --secondary: 12 6.5% 15.1%; 71 | --secondary-foreground: 60 9.1% 97.8%; 72 | --muted: 12 6.5% 15.1%; 73 | --muted-foreground: 24 5.4% 63.9%; 74 | --accent: 12 6.5% 15.1%; 75 | --accent-foreground: 60 9.1% 97.8%; 76 | --destructive: 0 62.8% 30.6%; 77 | --destructive-foreground: 60 9.1% 97.8%; 78 | --border: 12 6.5% 15.1%; 79 | --input: 12 6.5% 15.1%; 80 | --ring: 24 5.7% 82.9%; 81 | --chart-1: 220 70% 50%; 82 | --chart-2: 160 60% 45%; 83 | --chart-3: 30 80% 55%; 84 | --chart-4: 280 65% 60%; 85 | --chart-5: 340 75% 55%; 86 | } 87 | } 88 | 89 | @layer base { 90 | * { 91 | @apply border-border; 92 | } 93 | body { 94 | @apply bg-background text-foreground; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /challenge5/frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import localFont from 'next/font/local' 3 | import './globals.css' 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900' 9 | }) 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900' 14 | }) 15 | 16 | export const metadata: Metadata = { 17 | title: 'Conversational Assistant', 18 | description: 'Structured Outputs demo', 19 | icons: { 20 | icon: '/imgs/convex_icon.svg' 21 | } 22 | } 23 | 24 | export default function RootLayout({ 25 | children 26 | }: Readonly<{ 27 | children: React.ReactNode 28 | }>) { 29 | return ( 30 | 31 | 32 |
33 |
{children}
34 |
35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /challenge5/frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Assistant from '@/components/assistant' 2 | 3 | export default function Main() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /challenge5/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /challenge5/frontend/components/assistant.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import Chat from './chat' 4 | import useConversationStore from '@/stores/useConversationStore' 5 | import { Item, processMessages } from '@/lib/assistant' 6 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' 7 | 8 | const Assistant: React.FC = () => { 9 | const { chatMessages, addConversationItem, addChatMessage } = 10 | useConversationStore() 11 | 12 | const handleSendMessage = async (message: string) => { 13 | if (!message.trim()) return 14 | 15 | const userItem: Item = { 16 | type: 'message', 17 | role: 'user', 18 | content: message.trim() 19 | } 20 | const userMessage: ChatCompletionMessageParam = { 21 | role: 'user', 22 | content: message.trim() 23 | } 24 | 25 | try { 26 | addConversationItem(userMessage) 27 | addChatMessage(userItem) 28 | 29 | await processMessages() 30 | } catch (error) { 31 | console.error('Error processing message:', error) 32 | } 33 | } 34 | 35 | return ( 36 |
37 | 38 |
39 | ) 40 | } 41 | 42 | export default Assistant 43 | -------------------------------------------------------------------------------- /challenge5/frontend/components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Item } from '@/lib/assistant' 4 | import React, { useEffect, useRef, useState } from 'react' 5 | import ToolCall from './tool-call' 6 | import Message from './message' 7 | import VoiceMode from './voice-mode' 8 | 9 | interface ChatProps { 10 | items: Item[] 11 | onSendMessage: (message: string) => void 12 | } 13 | 14 | const Chat: React.FC = ({ items, onSendMessage }) => { 15 | const itemsEndRef = useRef(null) 16 | const [inputMessageText, setinputMessageText] = useState('') 17 | 18 | const scrollToBottom = () => { 19 | itemsEndRef.current?.scrollIntoView({ behavior: 'instant' }) 20 | } 21 | 22 | useEffect(() => { 23 | scrollToBottom() 24 | }, [items]) 25 | 26 | return ( 27 |
28 |
29 |
30 |
31 | {items.map((item, index) => ( 32 | 33 | {item.type === 'function_call' ? ( 34 | 38 | ) : ( 39 | 40 | )} 41 | 42 | ))} 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |