├── .env.example
├── app
├── favicon.ico
├── layout.tsx
├── api
│ └── chat
│ │ └── route.ts
├── page.tsx
└── globals.css
├── public
├── vercel.svg
├── window.svg
├── file.svg
├── globe.svg
└── next.svg
├── next.config.ts
├── postcss.config.mjs
├── lib
└── utils.ts
├── eslint.config.mjs
├── components.json
├── .gitignore
├── tsconfig.json
├── components
├── ui
│ ├── textarea.tsx
│ ├── label.tsx
│ ├── input.tsx
│ ├── badge.tsx
│ ├── tooltip.tsx
│ ├── alert.tsx
│ ├── scroll-area.tsx
│ ├── button.tsx
│ ├── card.tsx
│ └── dialog.tsx
├── use-scroll-to-bottom.tsx
├── steps.tsx
├── markdown.tsx
├── message.tsx
└── deep-research.tsx
├── ai
├── tools.ts
└── agents
│ └── deep-research.ts
├── README.md
├── package.json
└── tailwind.config.ts
/.env.example:
--------------------------------------------------------------------------------
1 | OPENAI_API_KEY=""
2 | EXA_API_KEY=""
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nicoalbanese/aie-deepresearch/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/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": "zinc",
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 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const geistSans = Geist({
6 | variable: "--font-geist-sans",
7 | subsets: ["latin"],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: "--font-geist-mono",
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: "Create Next App",
17 | description: "Generated by create next app",
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: Readonly<{
23 | children: React.ReactNode;
24 | }>) {
25 | return (
26 |
27 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/components/use-scroll-to-bottom.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, type RefObject } from 'react';
2 |
3 | export function useScrollToBottom(): [
4 | RefObject,
5 | RefObject,
6 | ] {
7 | const containerRef = useRef(null);
8 | const endRef = useRef(null);
9 |
10 | useEffect(() => {
11 | const container = containerRef.current;
12 | const end = endRef.current;
13 |
14 | if (container && end) {
15 | const observer = new MutationObserver(() => {
16 | end.scrollIntoView({ behavior: 'instant', block: 'end' });
17 | });
18 |
19 | observer.observe(container, {
20 | childList: true,
21 | subtree: true,
22 | attributes: true,
23 | characterData: true,
24 | });
25 |
26 | return () => observer.disconnect();
27 | }
28 | }, []);
29 |
30 | return [containerRef, endRef];
31 | }
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { deepResearchTool } from "@/ai/agents/deep-research";
2 | import { tools } from "@/ai/tools";
3 | import { openai } from "@ai-sdk/openai";
4 | import { createDataStreamResponse, streamText } from "ai";
5 |
6 | // Allow streaming responses up to 120 seconds
7 | export const maxDuration = 120;
8 |
9 | export async function POST(req: Request) {
10 | const { messages } = await req.json();
11 |
12 | return createDataStreamResponse({
13 | execute: async (dataStream) => {
14 | const result = streamText({
15 | model: openai("gpt-4o"),
16 | messages,
17 | system:
18 | "You are a helpful assistant. Do not repeat the results of deepResearch tool calls. You can report (max 2 sentences) that the tool has been used successfully. Do not call multiple tools at once.",
19 | tools: {
20 | ...tools,
21 | deepResearch: deepResearchTool(dataStream),
22 | },
23 | });
24 | result.mergeIntoDataStream(dataStream);
25 | },
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
19 |
28 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 |
--------------------------------------------------------------------------------
/ai/tools.ts:
--------------------------------------------------------------------------------
1 | import Exa from "exa-js";
2 | import "dotenv/config";
3 | import { tool as createTool } from "ai";
4 | import { z } from "zod";
5 |
6 | export const exa = new Exa(process.env.EXA_API_KEY);
7 |
8 | const webSearch = createTool({
9 | description: "Use this tool to search the web for information.",
10 | parameters: z.object({
11 | query: z
12 | .string()
13 | .min(1)
14 | .max(200)
15 | .describe(
16 | "The search query - be specific and include terms like 'vs', 'features', 'comparison' for better results",
17 | ),
18 | limit: z
19 | .number()
20 | .min(1)
21 | .max(10)
22 | .default(5)
23 | .describe("The number of results to return"),
24 | }),
25 | execute: async ({ query, limit }) => {
26 | const { results } = await exa.searchAndContents(query, {
27 | numResults: limit,
28 | startPublishedDate: new Date("2025-01-01").toISOString(),
29 | });
30 | // Process and clean the results
31 | return results.map((result) => ({
32 | title: result.title,
33 | url: result.url,
34 | snippet: result.text, // Limit snippet length
35 | domain: new URL(result.url).hostname, // Extract domain for source context
36 | date: result.publishedDate || "Date not available", // Include publish date when available
37 | }));
38 | },
39 | });
40 |
41 | export const tools = {
42 | webSearch,
43 | };
44 |
--------------------------------------------------------------------------------
/components/steps.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { motion, AnimatePresence } from "framer-motion";
3 |
4 | export interface StatusUpdate {
5 | title: string;
6 | description: string;
7 | }
8 |
9 | interface StatusProps {
10 | updates: StatusUpdate[];
11 | className?: string;
12 | }
13 |
14 | export function DeepResearchStatus({ updates, className }: StatusProps) {
15 | if (updates.length === 0) return null;
16 | const currentUpdate = updates[updates.length - 1];
17 |
18 | return (
19 |
20 |
21 |
32 |
33 | {currentUpdate.title}
34 |
35 | {/* {currentUpdate.description && (
36 |
37 | {currentUpdate.description}
38 |
39 | )} */}
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aie-prep-explore",
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 | "@ai-sdk/google": "^1.1.16",
13 | "@ai-sdk/openai": "^1.1.12",
14 | "@ai-sdk/perplexity": "^1.0.0",
15 | "@ai-sdk/react": "^1.1.16",
16 | "@radix-ui/react-dialog": "^1.1.6",
17 | "@radix-ui/react-label": "^2.1.2",
18 | "@radix-ui/react-scroll-area": "^1.2.3",
19 | "@radix-ui/react-slot": "^1.1.2",
20 | "@radix-ui/react-tooltip": "^1.1.8",
21 | "@tailwindcss/typography": "^0.5.16",
22 | "ai": "^4.1.45",
23 | "class-variance-authority": "^0.7.1",
24 | "classnames": "^2.5.1",
25 | "clsx": "^2.1.1",
26 | "exa-js": "^1.4.10",
27 | "fast-deep-equal": "^3.1.3",
28 | "framer-motion": "^12.4.3",
29 | "lucide-react": "^0.475.0",
30 | "marked": "^15.0.7",
31 | "next": "15.1.7",
32 | "react": "^19.0.0",
33 | "react-dom": "^19.0.0",
34 | "react-markdown": "^9.0.3",
35 | "remark-gfm": "^4.0.1",
36 | "tailwind-merge": "^3.0.1",
37 | "tailwindcss-animate": "^1.0.7",
38 | "zod": "^3.24.2"
39 | },
40 | "devDependencies": {
41 | "@eslint/eslintrc": "^3",
42 | "@types/node": "^20",
43 | "@types/react": "^19",
44 | "@types/react-dom": "^19",
45 | "dotenv": "^16.4.7",
46 | "eslint": "^9",
47 | "eslint-config-next": "15.1.7",
48 | "postcss": "^8",
49 | "tailwindcss": "^3.4.1",
50 | "tsx": "^4.19.2",
51 | "typescript": "^5"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { PreviewMessage } from "@/components/message";
4 | import { Input } from "@/components/ui/input";
5 | import { useScrollToBottom } from "@/components/use-scroll-to-bottom";
6 | import { useChat } from "@ai-sdk/react";
7 | import { ArrowRight } from "lucide-react";
8 |
9 | export default function Chat() {
10 | const { messages, input, handleInputChange, handleSubmit, error, isLoading } =
11 | useChat({
12 | maxSteps: 10,
13 | });
14 | const [containerRef, endRef] = useScrollToBottom();
15 |
16 | if (error) return {error.message}
;
17 |
18 | return (
19 |
20 |
25 | {messages.map((message) => (
26 |
31 | ))}
32 | {/* @ts-expect-error ref */}
33 |
34 |
35 |
36 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | import typography from '@tailwindcss/typography'
4 |
5 |
6 | export default {
7 | darkMode: ["class"],
8 | content: [
9 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
10 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
11 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
12 | ],
13 | theme: {
14 | extend: {
15 | colors: {
16 | background: "hsl(var(--background))",
17 | foreground: "hsl(var(--foreground))",
18 | card: {
19 | DEFAULT: "hsl(var(--card))",
20 | foreground: "hsl(var(--card-foreground))",
21 | },
22 | popover: {
23 | DEFAULT: "hsl(var(--popover))",
24 | foreground: "hsl(var(--popover-foreground))",
25 | },
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | muted: {
35 | DEFAULT: "hsl(var(--muted))",
36 | foreground: "hsl(var(--muted-foreground))",
37 | },
38 | accent: {
39 | DEFAULT: "hsl(var(--accent))",
40 | foreground: "hsl(var(--accent-foreground))",
41 | },
42 | destructive: {
43 | DEFAULT: "hsl(var(--destructive))",
44 | foreground: "hsl(var(--destructive-foreground))",
45 | },
46 | border: "hsl(var(--border))",
47 | input: "hsl(var(--input))",
48 | ring: "hsl(var(--ring))",
49 | chart: {
50 | "1": "hsl(var(--chart-1))",
51 | "2": "hsl(var(--chart-2))",
52 | "3": "hsl(var(--chart-3))",
53 | "4": "hsl(var(--chart-4))",
54 | "5": "hsl(var(--chart-5))",
55 | },
56 | },
57 | borderRadius: {
58 | lg: "var(--radius)",
59 | md: "calc(var(--radius) - 2px)",
60 | sm: "calc(var(--radius) - 4px)",
61 | },
62 | },
63 | },
64 | plugins: [typography()],
65 | } satisfies Config;
66 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLDivElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer base {
10 | :root {
11 | --background: 0 0% 100%;
12 | --foreground: 240 10% 3.9%;
13 | --card: 0 0% 100%;
14 | --card-foreground: 240 10% 3.9%;
15 | --popover: 0 0% 100%;
16 | --popover-foreground: 240 10% 3.9%;
17 | --primary: 240 5.9% 10%;
18 | --primary-foreground: 0 0% 98%;
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 | --muted: 240 4.8% 95.9%;
22 | --muted-foreground: 240 3.8% 46.1%;
23 | --accent: 240 4.8% 95.9%;
24 | --accent-foreground: 240 5.9% 10%;
25 | --destructive: 0 84.2% 60.2%;
26 | --destructive-foreground: 0 0% 98%;
27 | --border: 240 5.9% 90%;
28 | --input: 240 5.9% 90%;
29 | --ring: 240 10% 3.9%;
30 | --chart-1: 12 76% 61%;
31 | --chart-2: 173 58% 39%;
32 | --chart-3: 197 37% 24%;
33 | --chart-4: 43 74% 66%;
34 | --chart-5: 27 87% 67%;
35 | --radius: 0.5rem;
36 | }
37 | .dark {
38 | --background: 240 10% 3.9%;
39 | --foreground: 0 0% 98%;
40 | --card: 240 10% 3.9%;
41 | --card-foreground: 0 0% 98%;
42 | --popover: 240 10% 3.9%;
43 | --popover-foreground: 0 0% 98%;
44 | --primary: 0 0% 98%;
45 | --primary-foreground: 240 5.9% 10%;
46 | --secondary: 240 3.7% 15.9%;
47 | --secondary-foreground: 0 0% 98%;
48 | --muted: 240 3.7% 15.9%;
49 | --muted-foreground: 240 5% 64.9%;
50 | --accent: 240 3.7% 15.9%;
51 | --accent-foreground: 0 0% 98%;
52 | --destructive: 0 62.8% 30.6%;
53 | --destructive-foreground: 0 0% 98%;
54 | --border: 240 3.7% 15.9%;
55 | --input: 240 3.7% 15.9%;
56 | --ring: 240 4.9% 83.9%;
57 | --chart-1: 220 70% 50%;
58 | --chart-2: 160 60% 45%;
59 | --chart-3: 30 80% 55%;
60 | --chart-4: 280 65% 60%;
61 | --chart-5: 340 75% 55%;
62 | }
63 | }
64 |
65 | @layer base {
66 | * {
67 | @apply border-border;
68 | }
69 | body {
70 | @apply bg-background text-foreground;
71 | }
72 | }
73 |
74 | ::-webkit-scrollbar {
75 | width: 0;
76 | background: transparent;
77 | display: none;
78 | }
79 |
80 | * {
81 | scrollbar-width: none;
82 | -ms-overflow-style: none;
83 | }
--------------------------------------------------------------------------------
/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React, { memo } from "react";
3 | import ReactMarkdown, { type Components } from "react-markdown";
4 | import remarkGfm from "remark-gfm";
5 |
6 | const components: Partial = {
7 | pre: ({ children }) => <>{children}>,
8 | ol: ({ node, children, ...props }) => {
9 | return (
10 |
11 | {children}
12 |
13 | );
14 | },
15 | li: ({ node, children, ...props }) => {
16 | return (
17 |
18 | {children}
19 |
20 | );
21 | },
22 | ul: ({ node, children, ...props }) => {
23 | return (
24 |
27 | );
28 | },
29 | strong: ({ node, children, ...props }) => {
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | },
36 | a: ({ node, children, ...props }) => {
37 | return (
38 | // @ts-expect-error
39 |
45 | {children}
46 |
47 | );
48 | },
49 | h1: ({ node, children, ...props }) => {
50 | return (
51 |
52 | {children}
53 |
54 | );
55 | },
56 | h2: ({ node, children, ...props }) => {
57 | return (
58 |
59 | {children}
60 |
61 | );
62 | },
63 | h3: ({ node, children, ...props }) => {
64 | return (
65 |
66 | {children}
67 |
68 | );
69 | },
70 | h4: ({ node, children, ...props }) => {
71 | return (
72 |
73 | {children}
74 |
75 | );
76 | },
77 | h5: ({ node, children, ...props }) => {
78 | return (
79 |
80 | {children}
81 |
82 | );
83 | },
84 | h6: ({ node, children, ...props }) => {
85 | return (
86 |
87 | {children}
88 |
89 | );
90 | },
91 | };
92 |
93 | const remarkPlugins = [remarkGfm];
94 |
95 | const NonMemoizedMarkdown = ({ children }: { children: string }) => {
96 | return (
97 |
98 | {children}
99 |
100 | );
101 | };
102 |
103 | export const Markdown = memo(
104 | NonMemoizedMarkdown,
105 | (prevProps, nextProps) => prevProps.children === nextProps.children,
106 | );
107 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/message.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { Message } from "ai";
4 | import cx from "classnames";
5 | import { AnimatePresence, motion } from "framer-motion";
6 | import { memo } from "react";
7 | import equal from "fast-deep-equal";
8 |
9 | import { Markdown } from "./markdown";
10 | import { cn } from "@/lib/utils";
11 | import { SparklesIcon } from "lucide-react";
12 | import { DeepResearch } from "./deep-research";
13 |
14 | const PurePreviewMessage = ({
15 | message,
16 | }: {
17 | message: Message;
18 | isLoading: boolean;
19 | }) => {
20 | return (
21 |
22 |
28 |
34 | {message.role === "assistant" && (
35 |
40 | )}
41 |
42 |
43 | {message.parts?.map((part) => {
44 | switch (part.type) {
45 | case "text":
46 | return (
47 |
51 |
57 | {part.text as string}
58 |
59 |
60 | );
61 | case "tool-invocation":
62 | const { toolName, toolCallId, state } = part.toolInvocation;
63 | return (
64 |
70 | {toolName === "deepResearch" ? (
71 |
75 | ) : toolName === "webSearch" ? (
76 | state === "result" ? null : (
77 |
83 | Searching the web...
84 |
85 | )
86 | ) : null}
87 |
88 | );
89 |
90 | default:
91 | return null;
92 | }
93 | })}
94 |
95 |
96 |
97 |
98 | );
99 | };
100 |
101 | export const PreviewMessage = memo(
102 | PurePreviewMessage,
103 | (prevProps, nextProps) => {
104 | if (prevProps.isLoading !== nextProps.isLoading) return false;
105 | if (prevProps.message.reasoning !== nextProps.message.reasoning)
106 | return false;
107 | if (prevProps.message.annotations !== nextProps.message.annotations)
108 | return false;
109 | if (prevProps.message.content !== nextProps.message.content) return false;
110 | if (
111 | !equal(
112 | prevProps.message.toolInvocations,
113 | nextProps.message.toolInvocations,
114 | )
115 | )
116 | return false;
117 |
118 | return true;
119 | },
120 | );
121 |
--------------------------------------------------------------------------------
/components/deep-research.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogTrigger,
8 | } from "@/components/ui/dialog";
9 | import { ScrollArea } from "@/components/ui/scroll-area";
10 | import type { Research } from "@/ai/agents/deep-research";
11 | import type { JSONValue, ToolInvocation } from "ai";
12 | import { useEffect, useState } from "react";
13 | import { motion, AnimatePresence } from "framer-motion";
14 | import { FileText, LinkIcon } from "lucide-react";
15 | import { DeepResearchStatus, StatusUpdate } from "./steps";
16 | import { Markdown } from "./markdown";
17 |
18 | function formatElapsedTime(ms: number) {
19 | const seconds = Math.floor(ms / 1000);
20 | const minutes = Math.floor(seconds / 60);
21 | return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
22 | }
23 |
24 | export const DeepResearch = ({
25 | toolInvocation,
26 | annotations,
27 | }: {
28 | toolInvocation: ToolInvocation;
29 | annotations?: JSONValue[];
30 | }) => {
31 | const { state } = toolInvocation;
32 | const [startTime] = useState(Date.now());
33 | const [elapsedTime, setElapsedTime] = useState(0);
34 |
35 | useEffect(() => {
36 | if (state !== "result") {
37 | const interval = setInterval(() => {
38 | setElapsedTime(Date.now() - startTime);
39 | }, 1000);
40 |
41 | return () => clearInterval(interval);
42 | }
43 | }, [startTime, state]);
44 |
45 | const statusUpdates = annotations
46 | ?.filter(
47 | (
48 | annotation,
49 | ): annotation is { status: { title: string; description?: string } } =>
50 | annotation !== null &&
51 | typeof annotation === "object" &&
52 | "status" in annotation,
53 | )
54 | .map((update) => update.status);
55 |
56 | const steps: StatusUpdate[] = (statusUpdates || []).map((status) => ({
57 | title: status.title,
58 | description: status.description || "",
59 | }));
60 |
61 | const sourceUpdates = Array.from(
62 | new Set(
63 | annotations?.filter(
64 | (
65 | annotation,
66 | ): annotation is { source: { title: string; url: string } } =>
67 | annotation !== null &&
68 | typeof annotation === "object" &&
69 | "source" in annotation,
70 | ),
71 | ),
72 | ) as {
73 | source: { title: string; url: string };
74 | }[];
75 |
76 | if (state === "result") {
77 | const { result } = toolInvocation;
78 | const {
79 | report,
80 | research,
81 | }: { report: { report: string; title: string }; research: Research } =
82 | result;
83 |
84 | return (
85 |
91 |
92 |
93 |
94 |
95 |
Deep Research
96 |
97 |
98 | Completed in {formatElapsedTime(elapsedTime)}
99 |
100 |
101 |
102 |
125 |
126 | {research.sources && research.sources.length > 0 && (
127 |
181 | )}
182 |
183 |
184 | );
185 | }
186 |
187 | return (
188 |
194 |
195 |
196 |
197 |
198 |
Deep Research
199 |
200 |
201 | Time elapsed: {formatElapsedTime(elapsedTime)}
202 |
203 |
204 |
205 |
206 |
207 |
208 | {sourceUpdates && sourceUpdates.length > 0 && (
209 |
215 |
216 | Sources found so far:{" "}
217 | {
218 | Array.from(new Set(sourceUpdates.map((s) => s.source.url)))
219 | .length
220 | }
221 |
222 |
223 | )}
224 |
225 |
226 |
227 | );
228 | };
229 |
--------------------------------------------------------------------------------
/ai/agents/deep-research.ts:
--------------------------------------------------------------------------------
1 | // deepResearch(researchPrompt, depth (d), breadth (b))
2 | // --> generate b search queries
3 | // ----> for each search query, search web, and then pass results to LLM to generate a list of (3) learnings, and follow up questions (b - based on overall research prompt)
4 | // ----> recursively call deepResearch(followUpQuestion, depth-1, Math.ceil(breadth/2)) until d = 0.
5 |
6 | import Exa from "exa-js";
7 | import "dotenv/config";
8 | import { DataStreamWriter, generateObject, generateText, tool } from "ai";
9 | import { openai } from "@ai-sdk/openai";
10 | import { z } from "zod";
11 |
12 | export const SYSTEM_PROMPT = `You are an expert researcher. Today is ${new Date().toISOString()}. Follow these instructions when responding:
13 | - You may be asked to research subjects that is after your knowledge cutoff, assume the user is right when presented with news.
14 | - The user is a highly experienced analyst, no need to simplify it, be as detailed as possible and make sure your response is correct.
15 | - Be highly organized.
16 | - Suggest solutions that I didn't think about.
17 | - Be proactive and anticipate my needs.
18 | - Treat me as an expert in all subject matter.
19 | - Mistakes erode my trust, so be accurate and thorough.
20 | - Provide detailed explanations, I'm comfortable with lots of detail.
21 | - Value good arguments over authorities, the source is irrelevant.
22 | - Consider new technologies and contrarian ideas, not just the conventional wisdom.
23 | - You may use high levels of speculation or prediction, just flag it for me.
24 | - You must provide links to sources used. Ideally these are inline e.g. [this documentation](https://documentation.com/this)
25 | `;
26 |
27 | export const exa = new Exa(process.env.EXA_API_KEY);
28 |
29 | type SearchResult = {
30 | title: string;
31 | url: string;
32 | content: string;
33 | publishedDate: string;
34 | favicon: string;
35 | };
36 |
37 | export type Research = {
38 | learnings: string[];
39 | sources: SearchResult[];
40 | questionsExplored: string[];
41 | searchQueries: string[];
42 | };
43 |
44 | const searchWeb = async (query: string) => {
45 | const { results } = await exa.searchAndContents(query, {
46 | livecrawl: "always",
47 | numResults: 3,
48 | type: "keyword",
49 | });
50 | return results.map((r) => ({
51 | title: r.title,
52 | url: r.url,
53 | content: r.text,
54 | publishedDate: r.publishedDate,
55 | favicon: r.favicon,
56 | })) as SearchResult[];
57 | };
58 |
59 | const generateSearchQueries = async (
60 | query: string,
61 | breadth: number,
62 | learnings?: string[],
63 | ) => {
64 | const {
65 | object: { queries },
66 | } = await generateObject({
67 | system: SYSTEM_PROMPT,
68 | model: openai("o3-mini"),
69 | prompt: `Given the following prompt from the user, generate a list of SERP queries to research the topic. Ensure at least one is almost identical to the initial prompt. Return a maximum of ${breadth} queries, but feel free to return less if the original prompt is clear. Make sure each query is unique and not similar to each other: ${query}\n\n${
70 | learnings
71 | ? `Here are some learnings from previous research, use them to generate more specific queries: ${learnings.join(
72 | "\n",
73 | )}`
74 | : ""
75 | }`,
76 | schema: z.object({
77 | queries: z
78 | .array(
79 | z.object({
80 | query: z.string().describe("The SERP query"),
81 | researchGoal: z
82 | .string()
83 | .describe(
84 | "First talk about the goal of the research that this query is meant to accomplish, then go deeper into how to advance the research once the results are found, mention additional research directions. Be as specific as possible, especially for additional research directions.",
85 | ),
86 | }),
87 | )
88 | .describe(`List of SERP queries, max of ${breadth}`),
89 | }),
90 | });
91 | return queries;
92 | };
93 |
94 | const generateLearnings = async (
95 | query: string,
96 | results: SearchResult[],
97 | numberOfLearnings: number,
98 | numberOfFollowUpQuestions: number,
99 | ) => {
100 | const {
101 | object: { followUpQuestions, learnings },
102 | } = await generateObject({
103 | model: openai("o3-mini"),
104 | system: SYSTEM_PROMPT,
105 | prompt: `Given the following contents from a SERP search for the query ${query}, generate a list of learnings from the contents. Return a maximum of ${numberOfLearnings} learnings, but feel free to return less if the contents are clear. Make sure each learning is unique and not similar to each other. The learnings should be concise and to the point, as detailed and information dense as possible. Make sure to include any entities like people, places, companies, products, things, etc in the learnings, as well as any exact metrics, numbers, or dates. The learnings will be used to research the topic further.\n\n${results
106 | .map((content) => `\n${content.content}\n`)
107 | .join("\n")}`,
108 | schema: z.object({
109 | learnings: z
110 | .array(z.string())
111 | .describe(`List of learnings, max of ${numberOfLearnings}`),
112 | followUpQuestions: z
113 | .array(z.string())
114 | .describe(
115 | `List of follow-up questions to research the topic further, max of ${numberOfFollowUpQuestions}`,
116 | ),
117 | }),
118 | });
119 | return {
120 | learnings,
121 | followUpQuestions,
122 | };
123 | };
124 |
125 | const deepResearch = async (
126 | prompt: string,
127 | depth: number = 1,
128 | breadth: number = 3,
129 | accumulatedResearch: Research = {
130 | learnings: [],
131 | sources: [],
132 | questionsExplored: [],
133 | searchQueries: [],
134 | },
135 | dataStream: DataStreamWriter,
136 | ): Promise => {
137 | // Base case: regardless whether accumulatedResearch is present or empty, if depth is 0 we stop.
138 | if (depth === 0) {
139 | return accumulatedResearch;
140 | }
141 |
142 | dataStream.writeMessageAnnotation({
143 | status: { title: `Generating search queries for "${prompt}"` },
144 | });
145 | const searchQueries = await generateSearchQueries(
146 | prompt,
147 | breadth,
148 | accumulatedResearch.learnings,
149 | );
150 | dataStream.writeMessageAnnotation({
151 | status: { title: `Generated search queries for "${prompt}"` },
152 | });
153 |
154 | // Process each query and merge the results rather than overwrite
155 | const subResults = await Promise.all(
156 | searchQueries.map(async ({ query, researchGoal }) => {
157 | dataStream.writeMessageAnnotation({
158 | status: { title: `Searching the web for "${query}"` },
159 | });
160 | const results = await searchWeb(query);
161 | results.forEach(async (source) => {
162 | dataStream.writeMessageAnnotation({
163 | source: { title: source.title, url: source.url },
164 | });
165 | await new Promise((resolve) => setTimeout(resolve, 1000));
166 | });
167 |
168 | dataStream.writeMessageAnnotation({
169 | status: { title: `Analyzing search results for "${query}"` },
170 | });
171 |
172 | const { learnings, followUpQuestions } = await generateLearnings(
173 | query,
174 | results,
175 | 3,
176 | breadth,
177 | );
178 | const nextQuery =
179 | `Previous research goal: ${researchGoal}` +
180 | ` Follow-up directions: ${followUpQuestions.map((q) => `\n${q}`).join("")}`.trim();
181 |
182 | // Make the recursive call
183 | dataStream.writeMessageAnnotation({
184 | status: {
185 | title: `Diving deeper to understand "${followUpQuestions.slice(0, 3).join(", ")}"`,
186 | },
187 | });
188 | const subResearch = await deepResearch(
189 | nextQuery,
190 | depth - 1,
191 | Math.ceil(breadth / 2),
192 | undefined,
193 | dataStream,
194 | );
195 |
196 | subResearch.sources.forEach((source) => {
197 | dataStream.writeMessageAnnotation({
198 | source: { title: source.title, url: source.url },
199 | });
200 | });
201 |
202 | // Merge the research found at this level with the research in the child call.
203 | return {
204 | learnings,
205 | sources: results,
206 | questionsExplored: followUpQuestions,
207 | searchQueries: [query, ...subResearch.searchQueries],
208 | // Also merge in subResearch learnings, sources, and questions.
209 | subLearnings: subResearch.learnings,
210 | subSources: subResearch.sources,
211 | subQuestionsExplored: subResearch.questionsExplored,
212 | };
213 | }),
214 | );
215 | for (const res of subResults) {
216 | accumulatedResearch.learnings.push(...res.learnings, ...res.subLearnings);
217 | accumulatedResearch.sources.push(...res.sources, ...res.subSources);
218 | accumulatedResearch.questionsExplored.push(
219 | ...res.questionsExplored,
220 | ...res.subQuestionsExplored,
221 | );
222 | accumulatedResearch.searchQueries.push(...res.searchQueries);
223 | }
224 |
225 | return accumulatedResearch;
226 | };
227 |
228 | const generateReport = async (prompt: string, research: Research) => {
229 | const { learnings, sources, questionsExplored, searchQueries } = research;
230 | const { text: report } = await generateText({
231 | model: openai("o3-mini"),
232 | system: SYSTEM_PROMPT + "\n- Write in markdown sytax.",
233 | prompt: `Generate a comprehensive report focused on "${prompt}". The main research findings should be drawn from the learnings below, with the search queries and related questions explored serving as supplementary context. Focus on synthesizing the key insights into a coherent narrative around the main topic.
234 |
235 |
236 | ${learnings.map((l) => `\n${l}`).join("")}
237 |
238 |
239 |
240 | ${searchQueries.map((q) => `\n${q}`).join("")}
241 |
242 |
243 |
244 | ${questionsExplored.map((q) => `\n${q}`).join("")}
245 |
246 |
247 |
248 | ${sources.map((s) => `\n${JSON.stringify({ ...s, content: s.content.slice(0, 350) })}`).join("")}
249 |
250 | `,
251 | });
252 | const { object } = await generateObject({
253 | model: openai("gpt-4o-mini"),
254 | prompt:
255 | "Generate a punchy title (5 words) for the following report:\n\n" +
256 | report,
257 | schema: z.object({
258 | title: z.string(),
259 | }),
260 | });
261 |
262 | return { report, title: object.title };
263 | };
264 |
265 | export const deepResearchTool = (dataStream: DataStreamWriter) =>
266 | tool({
267 | description: "Use this tool to conduct a deep research on a given topic.",
268 | parameters: z.object({
269 | prompt: z
270 | .string()
271 | .min(1)
272 | .max(1000)
273 | .describe(
274 | "This should take the user's exact prompt. Extract from the context but do not infer or change in any way.",
275 | ),
276 | depth: z.number().min(1).max(3).default(1).describe("Default to 1 unless the user specifically references otherwise"),
277 | breadth: z.number().min(1).max(5).default(3).describe("Default to 3 unless the user specifically references otherwise"),
278 | }),
279 | execute: async ({ prompt, depth, breadth }) => {
280 | console.log({ prompt, depth, breadth });
281 | dataStream.writeMessageAnnotation({
282 | status: { title: "Beginning deep research" },
283 | });
284 | const research = await deepResearch(
285 | prompt,
286 | depth,
287 | breadth,
288 | undefined,
289 | dataStream,
290 | );
291 | dataStream.writeMessageAnnotation({
292 | status: { title: "Generating report" },
293 | });
294 | const report = await generateReport(prompt, research);
295 | dataStream.writeMessageAnnotation({
296 | status: { title: "Successfully generated report" },
297 | });
298 |
299 | return {
300 | report,
301 | research: {
302 | ...research,
303 | sources: Array.from(
304 | new Map(
305 | research.sources.map((s) => [
306 | s.url,
307 | { ...s, content: s.content.slice(0, 50) + "..." },
308 | ]),
309 | ).values(),
310 | ),
311 | },
312 | };
313 | },
314 | });
--------------------------------------------------------------------------------