[
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Output files\noutput.md\nreport.md\nanswer.md\n\n# Dependencies\nnode_modules\n.pnp\n.pnp.js\n\n# Local env files\n.env*\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# Testing\ncoverage\n\n# Turbo\n.turbo\n\n# Vercel\n.vercel\n\n# Build Outputs\n.next/\nout/\nbuild\ndist\n\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Misc\n.DS_Store\n*.pem\nbun.lockb"
  },
  {
    "path": ".nvmrc",
    "content": "v22\n"
  },
  {
    "path": ".prettierignore",
    "content": "*.hbs"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:18-alpine\n\nWORKDIR /app\n\nCOPY . .\nCOPY package.json ./\nCOPY .env.local ./.env.local\n\nRUN npm install\n\nCMD [\"npm\", \"run\", \"docker\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 David Zhang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Open Deep Research\n\nAn AI-powered research assistant that performs iterative, deep research on any topic by combining search engines, web scraping, and large language models.\n\nThe goal of this repo is to provide the simplest implementation of a deep research agent - e.g. an agent that can refine its research direction over time and deep dive into a topic. Goal is to keep the repo size at <500 LoC so it is easy to understand and build on top of.\n\nIf you like this project, please consider starring it and giving me a follow on [X/Twitter](https://x.com/dzhng). This project is sponsored by [Aomni](https://aomni.com).\n\n## How It Works\n\n```mermaid\nflowchart TB\n    subgraph Input\n        Q[User Query]\n        B[Breadth Parameter]\n        D[Depth Parameter]\n    end\n\n    DR[Deep Research] -->\n    SQ[SERP Queries] -->\n    PR[Process Results]\n\n    subgraph Results[Results]\n        direction TB\n        NL((Learnings))\n        ND((Directions))\n    end\n\n    PR --> NL\n    PR --> ND\n\n    DP{depth > 0?}\n\n    RD[\"Next Direction:\n    - Prior Goals\n    - New Questions\n    - Learnings\"]\n\n    MR[Markdown Report]\n\n    %% Main Flow\n    Q & B & D --> DR\n\n    %% Results to Decision\n    NL & ND --> DP\n\n    %% Circular Flow\n    DP -->|Yes| RD\n    RD -->|New Context| DR\n\n    %% Final Output\n    DP -->|No| MR\n\n    %% Styling\n    classDef input fill:#7bed9f,stroke:#2ed573,color:black\n    classDef process fill:#70a1ff,stroke:#1e90ff,color:black\n    classDef recursive fill:#ffa502,stroke:#ff7f50,color:black\n    classDef output fill:#ff4757,stroke:#ff6b81,color:black\n    classDef results fill:#a8e6cf,stroke:#3b7a57,color:black\n\n    class Q,B,D input\n    class DR,SQ,PR process\n    class DP,RD recursive\n    class MR output\n    class NL,ND results\n```\n\n## Features\n\n- **Iterative Research**: Performs deep research by iteratively generating search queries, processing results, and diving deeper based on findings\n- **Intelligent Query Generation**: Uses LLMs to generate targeted search queries based on research goals and previous findings\n- **Depth & Breadth Control**: Configurable parameters to control how wide (breadth) and deep (depth) the research goes\n- **Smart Follow-up**: Generates follow-up questions to better understand research needs\n- **Comprehensive Reports**: Produces detailed markdown reports with findings and sources\n- **Concurrent Processing**: Handles multiple searches and result processing in parallel for efficiency\n\n## Requirements\n\n- Node.js environment\n- API keys for:\n  - Firecrawl API (for web search and content extraction)\n  - OpenAI API (for o3 mini model)\n\n## Setup\n\n### Node.js\n\n1. Clone the repository\n2. Install dependencies:\n\n```bash\nnpm install\n```\n\n3. Set up environment variables in a `.env.local` file:\n\n```bash\nFIRECRAWL_KEY=\"your_firecrawl_key\"\n# If you want to use your self-hosted Firecrawl, add the following below:\n# FIRECRAWL_BASE_URL=\"http://localhost:3002\"\n\nOPENAI_KEY=\"your_openai_key\"\n```\n\nTo use local LLM, comment out `OPENAI_KEY` and instead uncomment `OPENAI_ENDPOINT` and `OPENAI_MODEL`:\n\n- Set `OPENAI_ENDPOINT` to the address of your local server (eg.\"http://localhost:1234/v1\")\n- Set `OPENAI_MODEL` to the name of the model loaded in your local server.\n\n### Docker\n\n1. Clone the repository\n2. Rename `.env.example` to `.env.local` and set your API keys\n\n3. Run `docker build -f Dockerfile`\n\n4. Run the Docker image:\n\n```bash\ndocker compose up -d\n```\n\n5. Execute `npm run docker` in the docker service:\n\n```bash\ndocker exec -it deep-research npm run docker\n```\n\n## Usage\n\nRun the research assistant:\n\n```bash\nnpm start\n```\n\nYou'll be prompted to:\n\n1. Enter your research query\n2. Specify research breadth (recommended: 3-10, default: 4)\n3. Specify research depth (recommended: 1-5, default: 2)\n4. Answer follow-up questions to refine the research direction\n\nThe system will then:\n\n1. Generate and execute search queries\n2. Process and analyze search results\n3. Recursively explore deeper based on findings\n4. Generate a comprehensive markdown report\n\nThe final report will be saved as `report.md` or `answer.md` in your working directory, depending on which modes you selected.\n\n### Concurrency\n\nIf you have a paid version of Firecrawl or a local version, feel free to increase the `ConcurrencyLimit` by setting the `CONCURRENCY_LIMIT` environment variable so it runs faster.\n\nIf you have a free version, you may sometimes run into rate limit errors, you can reduce the limit to 1 (but it will run a lot slower).\n\n### DeepSeek R1\n\nDeep research performs great on R1! We use [Fireworks](http://fireworks.ai) as the main provider for the R1 model. To use R1, simply set a Fireworks API key:\n\n```bash\nFIREWORKS_KEY=\"api_key\"\n```\n\nThe system will automatically switch over to use R1 instead of `o3-mini` when the key is detected.\n\n### Custom endpoints and models\n\nThere are 2 other optional env vars that lets you tweak the endpoint (for other OpenAI compatible APIs like OpenRouter or Gemini) as well as the model string.\n\n```bash\nOPENAI_ENDPOINT=\"custom_endpoint\"\nCUSTOM_MODEL=\"custom_model\"\n```\n\n## How It Works\n\n1. **Initial Setup**\n\n   - Takes user query and research parameters (breadth & depth)\n   - Generates follow-up questions to understand research needs better\n\n2. **Deep Research Process**\n\n   - Generates multiple SERP queries based on research goals\n   - Processes search results to extract key learnings\n   - Generates follow-up research directions\n\n3. **Recursive Exploration**\n\n   - If depth > 0, takes new research directions and continues exploration\n   - Each iteration builds on previous learnings\n   - Maintains context of research goals and findings\n\n4. **Report Generation**\n   - Compiles all findings into a comprehensive markdown report\n   - Includes all sources and references\n   - Organizes information in a clear, readable format\n  \n## Community implementations\n\n**Python**: https://github.com/Finance-LLMs/deep-research-python\n\n## License\n\nMIT License - feel free to use and modify as needed.\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  deep-research:\n    container_name: deep-research\n    build: .\n    env_file:\n      - .env.local\n    volumes:\n      -  ./:/app/\n    tty: true\n    stdin_open: true\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"open-deep-research\",\n  \"version\": \"0.0.1\",\n  \"main\": \"index.ts\",\n  \"scripts\": {\n    \"format\": \"prettier --write \\\"src/**/*.{ts,tsx}\\\"\",\n    \"tsx\": \"tsx --env-file=.env.local\",\n    \"start\": \"tsx --env-file=.env.local src/run.ts\",\n    \"api\": \"tsx --env-file=.env.local src/api.ts\",\n    \"docker\": \"tsx src/run.ts\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"description\": \"\",\n  \"devDependencies\": {\n    \"@ianvs/prettier-plugin-sort-imports\": \"^4.4.1\",\n    \"@types/cors\": \"^2.8.17\",\n    \"@types/express\": \"^4.17.21\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^22.13.0\",\n    \"@types/uuid\": \"^9.0.8\",\n    \"prettier\": \"^3.4.2\",\n    \"tsx\": \"^4.19.2\",\n    \"typescript\": \"^5.7.3\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/fireworks\": \"^0.1.14\",\n    \"@ai-sdk/openai\": \"^1.1.9\",\n    \"@mendable/firecrawl-js\": \"^1.16.0\",\n    \"ai\": \"^4.1.17\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.18.3\",\n    \"js-tiktoken\": \"^1.0.17\",\n    \"lodash-es\": \"^4.17.21\",\n    \"p-limit\": \"^6.2.0\",\n    \"uuid\": \"^9.0.1\",\n    \"zod\": \"^3.24.1\"\n  },\n  \"engines\": {\n    \"node\": \"22.x\"\n  }\n}\n"
  },
  {
    "path": "prettier.config.mjs",
    "content": "/** @type {import('prettier').Config} */\nexport default {\n  endOfLine: 'lf',\n  semi: true,\n  useTabs: false,\n  singleQuote: true,\n  arrowParens: 'avoid',\n  tabWidth: 2,\n  trailingComma: 'all',\n  importOrder: [\n    '^(react/(.*)$)|^(react$)',\n    '^(next/(.*)$)|^(next$)',\n    '<THIRD_PARTY_MODULES>',\n    '',\n    '@repo/(.*)$',\n    '',\n    '^@/(.*)$',\n    '',\n    '^[./]',\n  ],\n  importOrderParserPlugins: ['typescript', 'jsx'],\n  importOrderTypeScriptVersion: '5.7.2',\n  plugins: ['@ianvs/prettier-plugin-sort-imports'],\n};\n"
  },
  {
    "path": "src/ai/providers.ts",
    "content": "import { createFireworks } from '@ai-sdk/fireworks';\nimport { createOpenAI } from '@ai-sdk/openai';\nimport {\n  extractReasoningMiddleware,\n  LanguageModelV1,\n  wrapLanguageModel,\n} from 'ai';\nimport { getEncoding } from 'js-tiktoken';\n\nimport { RecursiveCharacterTextSplitter } from './text-splitter';\n\n// Providers\nconst openai = process.env.OPENAI_KEY\n  ? createOpenAI({\n      apiKey: process.env.OPENAI_KEY,\n      baseURL: process.env.OPENAI_ENDPOINT || 'https://api.openai.com/v1',\n    })\n  : undefined;\n\nconst fireworks = process.env.FIREWORKS_KEY\n  ? createFireworks({\n      apiKey: process.env.FIREWORKS_KEY,\n    })\n  : undefined;\n\nconst customModel = process.env.CUSTOM_MODEL\n  ? openai?.(process.env.CUSTOM_MODEL, {\n      structuredOutputs: true,\n    })\n  : undefined;\n\n// Models\n\nconst o3MiniModel = openai?.('o3-mini', {\n  reasoningEffort: 'medium',\n  structuredOutputs: true,\n});\n\nconst deepSeekR1Model = fireworks\n  ? wrapLanguageModel({\n      model: fireworks(\n        'accounts/fireworks/models/deepseek-r1',\n      ) as LanguageModelV1,\n      middleware: extractReasoningMiddleware({ tagName: 'think' }),\n    })\n  : undefined;\n\nexport function getModel(): LanguageModelV1 {\n  if (customModel) {\n    return customModel;\n  }\n\n  const model = deepSeekR1Model ?? o3MiniModel;\n  if (!model) {\n    throw new Error('No model found');\n  }\n\n  return model as LanguageModelV1;\n}\n\nconst MinChunkSize = 140;\nconst encoder = getEncoding('o200k_base');\n\n// trim prompt to maximum context size\nexport function trimPrompt(\n  prompt: string,\n  contextSize = Number(process.env.CONTEXT_SIZE) || 128_000,\n) {\n  if (!prompt) {\n    return '';\n  }\n\n  const length = encoder.encode(prompt).length;\n  if (length <= contextSize) {\n    return prompt;\n  }\n\n  const overflowTokens = length - contextSize;\n  // on average it's 3 characters per token, so multiply by 3 to get a rough estimate of the number of characters\n  const chunkSize = prompt.length - overflowTokens * 3;\n  if (chunkSize < MinChunkSize) {\n    return prompt.slice(0, MinChunkSize);\n  }\n\n  const splitter = new RecursiveCharacterTextSplitter({\n    chunkSize,\n    chunkOverlap: 0,\n  });\n  const trimmedPrompt = splitter.splitText(prompt)[0] ?? '';\n\n  // last catch, there's a chance that the trimmed prompt is same length as the original prompt, due to how tokens are split & innerworkings of the splitter, handle this case by just doing a hard cut\n  if (trimmedPrompt.length === prompt.length) {\n    return trimPrompt(prompt.slice(0, chunkSize), contextSize);\n  }\n\n  // recursively trim until the prompt is within the context size\n  return trimPrompt(trimmedPrompt, contextSize);\n}\n"
  },
  {
    "path": "src/ai/text-splitter.test.ts",
    "content": "import assert from 'node:assert';\nimport { describe, it, beforeEach } from 'node:test';\nimport { RecursiveCharacterTextSplitter } from './text-splitter';\n\ndescribe('RecursiveCharacterTextSplitter', () => {\n  let splitter: RecursiveCharacterTextSplitter;\n\n  beforeEach(() => {\n    splitter = new RecursiveCharacterTextSplitter({\n      chunkSize: 50,\n      chunkOverlap: 10,\n    });\n  });\n\n  it('Should correctly split text by separators', () => {\n    const text = 'Hello world, this is a test of the recursive text splitter.';\n\n    // Test with initial chunkSize\n    assert.deepEqual(\n      splitter.splitText(text),\n      ['Hello world', 'this is a test of the recursive text splitter']\n    );\n\n    // Test with updated chunkSize\n    splitter.chunkSize = 100;\n    assert.deepEqual(\n      splitter.splitText(\n        'Hello world, this is a test of the recursive text splitter. If I have a period, it should split along the period.'\n      ),\n      [\n        'Hello world, this is a test of the recursive text splitter',\n        'If I have a period, it should split along the period.',\n      ]\n    );\n\n    // Test with another updated chunkSize\n    splitter.chunkSize = 110;\n    assert.deepEqual(\n      splitter.splitText(\n        'Hello world, this is a test of the recursive text splitter. If I have a period, it should split along the period.\\nOr, if there is a new line, it should prioritize splitting on new lines instead.'\n      ),\n      [\n        'Hello world, this is a test of the recursive text splitter',\n        'If I have a period, it should split along the period.',\n        'Or, if there is a new line, it should prioritize splitting on new lines instead.',\n      ]\n    );\n  });\n\n  it('Should handle empty string', () => {\n    assert.deepEqual(splitter.splitText(''), []);\n  });\n\n  it('Should handle special characters and large texts', () => {\n    const largeText = 'A'.repeat(1000);\n    splitter.chunkSize = 200;\n    assert.deepEqual(\n      splitter.splitText(largeText),\n      Array(5).fill('A'.repeat(200))\n    );\n\n    const specialCharText = 'Hello!@# world$%^ &*( this) is+ a-test';\n    assert.deepEqual(\n      splitter.splitText(specialCharText),\n      ['Hello!@#', 'world$%^', '&*( this)', 'is+', 'a-test']\n    );\n  });\n\n  it('Should handle chunkSize equal to chunkOverlap', () => {\n    splitter.chunkSize = 50;\n    splitter.chunkOverlap = 50;\n    assert.throws(\n      () => splitter.splitText('Invalid configuration'),\n      new Error('Cannot have chunkOverlap >= chunkSize')\n    );\n  });\n});\n"
  },
  {
    "path": "src/ai/text-splitter.ts",
    "content": "interface TextSplitterParams {\n  chunkSize: number;\n\n  chunkOverlap: number;\n}\n\nabstract class TextSplitter implements TextSplitterParams {\n  chunkSize = 1000;\n  chunkOverlap = 200;\n\n  constructor(fields?: Partial<TextSplitterParams>) {\n    this.chunkSize = fields?.chunkSize ?? this.chunkSize;\n    this.chunkOverlap = fields?.chunkOverlap ?? this.chunkOverlap;\n    if (this.chunkOverlap >= this.chunkSize) {\n      throw new Error('Cannot have chunkOverlap >= chunkSize');\n    }\n  }\n\n  abstract splitText(text: string): string[];\n\n  createDocuments(texts: string[]): string[] {\n    const documents: string[] = [];\n    for (let i = 0; i < texts.length; i += 1) {\n      const text = texts[i];\n      for (const chunk of this.splitText(text!)) {\n        documents.push(chunk);\n      }\n    }\n    return documents;\n  }\n\n  splitDocuments(documents: string[]): string[] {\n    return this.createDocuments(documents);\n  }\n\n  private joinDocs(docs: string[], separator: string): string | null {\n    const text = docs.join(separator).trim();\n    return text === '' ? null : text;\n  }\n\n  mergeSplits(splits: string[], separator: string): string[] {\n    const docs: string[] = [];\n    const currentDoc: string[] = [];\n    let total = 0;\n    for (const d of splits) {\n      const _len = d.length;\n      if (total + _len >= this.chunkSize) {\n        if (total > this.chunkSize) {\n          console.warn(\n            `Created a chunk of size ${total}, +\nwhich is longer than the specified ${this.chunkSize}`,\n          );\n        }\n        if (currentDoc.length > 0) {\n          const doc = this.joinDocs(currentDoc, separator);\n          if (doc !== null) {\n            docs.push(doc);\n          }\n          // Keep on popping if:\n          // - we have a larger chunk than in the chunk overlap\n          // - or if we still have any chunks and the length is long\n          while (\n            total > this.chunkOverlap ||\n            (total + _len > this.chunkSize && total > 0)\n          ) {\n            total -= currentDoc[0]!.length;\n            currentDoc.shift();\n          }\n        }\n      }\n      currentDoc.push(d);\n      total += _len;\n    }\n    const doc = this.joinDocs(currentDoc, separator);\n    if (doc !== null) {\n      docs.push(doc);\n    }\n    return docs;\n  }\n}\n\nexport interface RecursiveCharacterTextSplitterParams\n  extends TextSplitterParams {\n  separators: string[];\n}\n\nexport class RecursiveCharacterTextSplitter\n  extends TextSplitter\n  implements RecursiveCharacterTextSplitterParams\n{\n  separators: string[] = ['\\n\\n', '\\n', '.', ',', '>', '<', ' ', ''];\n\n  constructor(fields?: Partial<RecursiveCharacterTextSplitterParams>) {\n    super(fields);\n    this.separators = fields?.separators ?? this.separators;\n  }\n\n  splitText(text: string): string[] {\n    const finalChunks: string[] = [];\n\n    // Get appropriate separator to use\n    let separator: string = this.separators[this.separators.length - 1]!;\n    for (const s of this.separators) {\n      if (s === '') {\n        separator = s;\n        break;\n      }\n      if (text.includes(s)) {\n        separator = s;\n        break;\n      }\n    }\n\n    // Now that we have the separator, split the text\n    let splits: string[];\n    if (separator) {\n      splits = text.split(separator);\n    } else {\n      splits = text.split('');\n    }\n\n    // Now go merging things, recursively splitting longer texts.\n    let goodSplits: string[] = [];\n    for (const s of splits) {\n      if (s.length < this.chunkSize) {\n        goodSplits.push(s);\n      } else {\n        if (goodSplits.length) {\n          const mergedText = this.mergeSplits(goodSplits, separator);\n          finalChunks.push(...mergedText);\n          goodSplits = [];\n        }\n        const otherInfo = this.splitText(s);\n        finalChunks.push(...otherInfo);\n      }\n    }\n    if (goodSplits.length) {\n      const mergedText = this.mergeSplits(goodSplits, separator);\n      finalChunks.push(...mergedText);\n    }\n    return finalChunks;\n  }\n}\n"
  },
  {
    "path": "src/api.ts",
    "content": "import cors from 'cors';\nimport express, { Request, Response } from 'express';\n\nimport { deepResearch, writeFinalAnswer,writeFinalReport } from './deep-research';\n\nconst app = express();\nconst port = process.env.PORT || 3051;\n\n// Middleware\napp.use(cors());\napp.use(express.json());\n\n// Helper function for consistent logging\nfunction log(...args: any[]) {\n  console.log(...args);\n}\n\n// API endpoint to run research\napp.post('/api/research', async (req: Request, res: Response) => {\n  try {\n    const { query, depth = 3, breadth = 3 } = req.body;\n\n    if (!query) {\n      return res.status(400).json({ error: 'Query is required' });\n    }\n\n    log('\\nStarting research...\\n');\n\n    const { learnings, visitedUrls } = await deepResearch({\n      query,\n      breadth,\n      depth,\n    });\n\n    log(`\\n\\nLearnings:\\n\\n${learnings.join('\\n')}`);\n    log(\n      `\\n\\nVisited URLs (${visitedUrls.length}):\\n\\n${visitedUrls.join('\\n')}`,\n    );\n\n    const answer = await writeFinalAnswer({\n      prompt: query,\n      learnings,\n    });\n\n    // Return the results\n    return res.json({\n      success: true,\n      answer,\n      learnings,\n      visitedUrls,\n    });\n  } catch (error: unknown) {\n    console.error('Error in research API:', error);\n    return res.status(500).json({\n      error: 'An error occurred during research',\n      message: error instanceof Error ? error.message : String(error),\n    });\n  }\n});\n\n// generate report API\napp.post('/api/generate-report',async(req:Request,res:Response)=>{\n  try{\n    const {query,depth = 3,breadth=3 } = req.body;\n    if(!query){\n      return res.status(400).json({error:'Query is required'});\n    }\n    log('\\n Starting research...\\n')\n    const {learnings,visitedUrls} = await deepResearch({\n      query,\n      breadth,\n      depth\n    });\n    log(`\\n\\nLearnings:\\n\\n${learnings.join('\\n')}`);\n    log(\n      `\\n\\nVisited URLs (${visitedUrls.length}):\\n\\n${visitedUrls.join('\\n')}`,\n    );\n    const report = await writeFinalReport({\n      prompt:query,\n      learnings,\n      visitedUrls\n    });\n\n    return report\n    \n  }catch(error:unknown){\n    console.error(\"Error in generate report API:\",error)\n    return res.status(500).json({\n      error:'An error occurred during research',\n      message:error instanceof Error? error.message: String(error),\n    })\n  }\n})\n\n\n\n// Start the server\napp.listen(port, () => {\n  console.log(`Deep Research API running on port ${port}`);\n});\n\nexport default app;\n"
  },
  {
    "path": "src/deep-research.ts",
    "content": "import FirecrawlApp, { SearchResponse } from '@mendable/firecrawl-js';\nimport { generateObject } from 'ai';\nimport { compact } from 'lodash-es';\nimport pLimit from 'p-limit';\nimport { z } from 'zod';\n\nimport { getModel, trimPrompt } from './ai/providers';\nimport { systemPrompt } from './prompt';\n\nfunction log(...args: any[]) {\n  console.log(...args);\n}\n\nexport type ResearchProgress = {\n  currentDepth: number;\n  totalDepth: number;\n  currentBreadth: number;\n  totalBreadth: number;\n  currentQuery?: string;\n  totalQueries: number;\n  completedQueries: number;\n};\n\ntype ResearchResult = {\n  learnings: string[];\n  visitedUrls: string[];\n};\n\n// increase this if you have higher API rate limits\nconst ConcurrencyLimit = Number(process.env.FIRECRAWL_CONCURRENCY) || 2;\n\n// Initialize Firecrawl with optional API key and optional base url\n\nconst firecrawl = new FirecrawlApp({\n  apiKey: process.env.FIRECRAWL_KEY ?? '',\n  apiUrl: process.env.FIRECRAWL_BASE_URL,\n});\n\n// take en user query, return a list of SERP queries\nasync function generateSerpQueries({\n  query,\n  numQueries = 3,\n  learnings,\n}: {\n  query: string;\n  numQueries?: number;\n\n  // optional, if provided, the research will continue from the last learning\n  learnings?: string[];\n}) {\n  const res = await generateObject({\n    model: getModel(),\n    system: systemPrompt(),\n    prompt: `Given the following prompt from the user, generate a list of SERP queries to research the topic. Return a maximum of ${numQueries} 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: <prompt>${query}</prompt>\\n\\n${\n      learnings\n        ? `Here are some learnings from previous research, use them to generate more specific queries: ${learnings.join(\n            '\\n',\n          )}`\n        : ''\n    }`,\n    schema: z.object({\n      queries: z\n        .array(\n          z.object({\n            query: z.string().describe('The SERP query'),\n            researchGoal: z\n              .string()\n              .describe(\n                '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.',\n              ),\n          }),\n        )\n        .describe(`List of SERP queries, max of ${numQueries}`),\n    }),\n  });\n  log(`Created ${res.object.queries.length} queries`, res.object.queries);\n\n  return res.object.queries.slice(0, numQueries);\n}\n\nasync function processSerpResult({\n  query,\n  result,\n  numLearnings = 3,\n  numFollowUpQuestions = 3,\n}: {\n  query: string;\n  result: SearchResponse;\n  numLearnings?: number;\n  numFollowUpQuestions?: number;\n}) {\n  const contents = compact(result.data.map(item => item.markdown)).map(content =>\n    trimPrompt(content, 25_000),\n  );\n  log(`Ran ${query}, found ${contents.length} contents`);\n\n  const res = await generateObject({\n    model: getModel(),\n    abortSignal: AbortSignal.timeout(60_000),\n    system: systemPrompt(),\n    prompt: trimPrompt(\n      `Given the following contents from a SERP search for the query <query>${query}</query>, generate a list of learnings from the contents. Return a maximum of ${numLearnings} 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<contents>${contents\n        .map(content => `<content>\\n${content}\\n</content>`)\n        .join('\\n')}</contents>`,\n    ),\n    schema: z.object({\n      learnings: z.array(z.string()).describe(`List of learnings, max of ${numLearnings}`),\n      followUpQuestions: z\n        .array(z.string())\n        .describe(\n          `List of follow-up questions to research the topic further, max of ${numFollowUpQuestions}`,\n        ),\n    }),\n  });\n  log(`Created ${res.object.learnings.length} learnings`, res.object.learnings);\n\n  return res.object;\n}\n\nexport async function writeFinalReport({\n  prompt,\n  learnings,\n  visitedUrls,\n}: {\n  prompt: string;\n  learnings: string[];\n  visitedUrls: string[];\n}) {\n  const learningsString = learnings\n    .map(learning => `<learning>\\n${learning}\\n</learning>`)\n    .join('\\n');\n\n  const res = await generateObject({\n    model: getModel(),\n    system: systemPrompt(),\n    prompt: trimPrompt(\n      `Given the following prompt from the user, write a final report on the topic using the learnings from research. Make it as as detailed as possible, aim for 3 or more pages, include ALL the learnings from research:\\n\\n<prompt>${prompt}</prompt>\\n\\nHere are all the learnings from previous research:\\n\\n<learnings>\\n${learningsString}\\n</learnings>`,\n    ),\n    schema: z.object({\n      reportMarkdown: z.string().describe('Final report on the topic in Markdown'),\n    }),\n  });\n\n  // Append the visited URLs section to the report\n  const urlsSection = `\\n\\n## Sources\\n\\n${visitedUrls.map(url => `- ${url}`).join('\\n')}`;\n  return res.object.reportMarkdown + urlsSection;\n}\n\nexport async function writeFinalAnswer({\n  prompt,\n  learnings,\n}: {\n  prompt: string;\n  learnings: string[];\n}) {\n  const learningsString = learnings\n    .map(learning => `<learning>\\n${learning}\\n</learning>`)\n    .join('\\n');\n\n  const res = await generateObject({\n    model: getModel(),\n    system: systemPrompt(),\n    prompt: trimPrompt(\n      `Given the following prompt from the user, write a final answer on the topic using the learnings from research. Follow the format specified in the prompt. Do not yap or babble or include any other text than the answer besides the format specified in the prompt. Keep the answer as concise as possible - usually it should be just a few words or maximum a sentence. Try to follow the format specified in the prompt (for example, if the prompt is using Latex, the answer should be in Latex. If the prompt gives multiple answer choices, the answer should be one of the choices).\\n\\n<prompt>${prompt}</prompt>\\n\\nHere are all the learnings from research on the topic that you can use to help answer the prompt:\\n\\n<learnings>\\n${learningsString}\\n</learnings>`,\n    ),\n    schema: z.object({\n      exactAnswer: z\n        .string()\n        .describe('The final answer, make it short and concise, just the answer, no other text'),\n    }),\n  });\n\n  return res.object.exactAnswer;\n}\n\nexport async function deepResearch({\n  query,\n  breadth,\n  depth,\n  learnings = [],\n  visitedUrls = [],\n  onProgress,\n}: {\n  query: string;\n  breadth: number;\n  depth: number;\n  learnings?: string[];\n  visitedUrls?: string[];\n  onProgress?: (progress: ResearchProgress) => void;\n}): Promise<ResearchResult> {\n  const progress: ResearchProgress = {\n    currentDepth: depth,\n    totalDepth: depth,\n    currentBreadth: breadth,\n    totalBreadth: breadth,\n    totalQueries: 0,\n    completedQueries: 0,\n  };\n\n  const reportProgress = (update: Partial<ResearchProgress>) => {\n    Object.assign(progress, update);\n    onProgress?.(progress);\n  };\n\n  const serpQueries = await generateSerpQueries({\n    query,\n    learnings,\n    numQueries: breadth,\n  });\n\n  reportProgress({\n    totalQueries: serpQueries.length,\n    currentQuery: serpQueries[0]?.query,\n  });\n\n  const limit = pLimit(ConcurrencyLimit);\n\n  const results = await Promise.all(\n    serpQueries.map(serpQuery =>\n      limit(async () => {\n        try {\n          const result = await firecrawl.search(serpQuery.query, {\n            timeout: 15000,\n            limit: 5,\n            scrapeOptions: { formats: ['markdown'] },\n          });\n\n          // Collect URLs from this search\n          const newUrls = compact(result.data.map(item => item.url));\n          const newBreadth = Math.ceil(breadth / 2);\n          const newDepth = depth - 1;\n\n          const newLearnings = await processSerpResult({\n            query: serpQuery.query,\n            result,\n            numFollowUpQuestions: newBreadth,\n          });\n          const allLearnings = [...learnings, ...newLearnings.learnings];\n          const allUrls = [...visitedUrls, ...newUrls];\n\n          if (newDepth > 0) {\n            log(`Researching deeper, breadth: ${newBreadth}, depth: ${newDepth}`);\n\n            reportProgress({\n              currentDepth: newDepth,\n              currentBreadth: newBreadth,\n              completedQueries: progress.completedQueries + 1,\n              currentQuery: serpQuery.query,\n            });\n\n            const nextQuery = `\n            Previous research goal: ${serpQuery.researchGoal}\n            Follow-up research directions: ${newLearnings.followUpQuestions.map(q => `\\n${q}`).join('')}\n          `.trim();\n\n            return deepResearch({\n              query: nextQuery,\n              breadth: newBreadth,\n              depth: newDepth,\n              learnings: allLearnings,\n              visitedUrls: allUrls,\n              onProgress,\n            });\n          } else {\n            reportProgress({\n              currentDepth: 0,\n              completedQueries: progress.completedQueries + 1,\n              currentQuery: serpQuery.query,\n            });\n            return {\n              learnings: allLearnings,\n              visitedUrls: allUrls,\n            };\n          }\n        } catch (e: any) {\n          if (e.message && e.message.includes('Timeout')) {\n            log(`Timeout error running query: ${serpQuery.query}: `, e);\n          } else {\n            log(`Error running query: ${serpQuery.query}: `, e);\n          }\n          return {\n            learnings: [],\n            visitedUrls: [],\n          };\n        }\n      }),\n    ),\n  );\n\n  return {\n    learnings: [...new Set(results.flatMap(r => r.learnings))],\n    visitedUrls: [...new Set(results.flatMap(r => r.visitedUrls))],\n  };\n}\n"
  },
  {
    "path": "src/feedback.ts",
    "content": "import { generateObject } from 'ai';\nimport { z } from 'zod';\n\nimport { getModel } from './ai/providers';\nimport { systemPrompt } from './prompt';\n\nexport async function generateFeedback({\n  query,\n  numQuestions = 3,\n}: {\n  query: string;\n  numQuestions?: number;\n}) {\n  const userFeedback = await generateObject({\n    model: getModel(),\n    system: systemPrompt(),\n    prompt: `Given the following query from the user, ask some follow up questions to clarify the research direction. Return a maximum of ${numQuestions} questions, but feel free to return less if the original query is clear: <query>${query}</query>`,\n    schema: z.object({\n      questions: z\n        .array(z.string())\n        .describe(\n          `Follow up questions to clarify the research direction, max of ${numQuestions}`,\n        ),\n    }),\n  });\n\n  return userFeedback.object.questions.slice(0, numQuestions);\n}\n"
  },
  {
    "path": "src/prompt.ts",
    "content": "export const systemPrompt = () => {\n  const now = new Date().toISOString();\n  return `You are an expert researcher. Today is ${now}. Follow these instructions when responding:\n  - You may be asked to research subjects that is after your knowledge cutoff, assume the user is right when presented with news.\n  - The user is a highly experienced analyst, no need to simplify it, be as detailed as possible and make sure your response is correct.\n  - Be highly organized.\n  - Suggest solutions that I didn't think about.\n  - Be proactive and anticipate my needs.\n  - Treat me as an expert in all subject matter.\n  - Mistakes erode my trust, so be accurate and thorough.\n  - Provide detailed explanations, I'm comfortable with lots of detail.\n  - Value good arguments over authorities, the source is irrelevant.\n  - Consider new technologies and contrarian ideas, not just the conventional wisdom.\n  - You may use high levels of speculation or prediction, just flag it for me.`;\n};\n"
  },
  {
    "path": "src/run.ts",
    "content": "import * as fs from 'fs/promises';\nimport * as readline from 'readline';\n\nimport { getModel } from './ai/providers';\nimport {\n  deepResearch,\n  writeFinalAnswer,\n  writeFinalReport,\n} from './deep-research';\nimport { generateFeedback } from './feedback';\n\n// Helper function for consistent logging\nfunction log(...args: any[]) {\n  console.log(...args);\n}\n\nconst rl = readline.createInterface({\n  input: process.stdin,\n  output: process.stdout,\n});\n\n// Helper function to get user input\nfunction askQuestion(query: string): Promise<string> {\n  return new Promise(resolve => {\n    rl.question(query, answer => {\n      resolve(answer);\n    });\n  });\n}\n\n// run the agent\nasync function run() {\n  console.log('Using model: ', getModel().modelId);\n\n  // Get initial query\n  const initialQuery = await askQuestion('What would you like to research? ');\n\n  // Get breath and depth parameters\n  const breadth =\n    parseInt(\n      await askQuestion(\n        'Enter research breadth (recommended 2-10, default 4): ',\n      ),\n      10,\n    ) || 4;\n  const depth =\n    parseInt(\n      await askQuestion('Enter research depth (recommended 1-5, default 2): '),\n      10,\n    ) || 2;\n  const isReport =\n    (await askQuestion(\n      'Do you want to generate a long report or a specific answer? (report/answer, default report): ',\n    )) !== 'answer';\n\n  let combinedQuery = initialQuery;\n  if (isReport) {\n    log(`Creating research plan...`);\n\n    // Generate follow-up questions\n    const followUpQuestions = await generateFeedback({\n      query: initialQuery,\n    });\n\n    log(\n      '\\nTo better understand your research needs, please answer these follow-up questions:',\n    );\n\n    // Collect answers to follow-up questions\n    const answers: string[] = [];\n    for (const question of followUpQuestions) {\n      const answer = await askQuestion(`\\n${question}\\nYour answer: `);\n      answers.push(answer);\n    }\n\n    // Combine all information for deep research\n    combinedQuery = `\nInitial Query: ${initialQuery}\nFollow-up Questions and Answers:\n${followUpQuestions.map((q: string, i: number) => `Q: ${q}\\nA: ${answers[i]}`).join('\\n')}\n`;\n  }\n\n  log('\\nStarting research...\\n');\n\n  const { learnings, visitedUrls } = await deepResearch({\n    query: combinedQuery,\n    breadth,\n    depth,\n  });\n\n  log(`\\n\\nLearnings:\\n\\n${learnings.join('\\n')}`);\n  log(`\\n\\nVisited URLs (${visitedUrls.length}):\\n\\n${visitedUrls.join('\\n')}`);\n  log('Writing final report...');\n\n  if (isReport) {\n    const report = await writeFinalReport({\n      prompt: combinedQuery,\n      learnings,\n      visitedUrls,\n    });\n\n    await fs.writeFile('report.md', report, 'utf-8');\n    console.log(`\\n\\nFinal Report:\\n\\n${report}`);\n    console.log('\\nReport has been saved to report.md');\n  } else {\n    const answer = await writeFinalAnswer({\n      prompt: combinedQuery,\n      learnings,\n    });\n\n    await fs.writeFile('answer.md', answer, 'utf-8');\n    console.log(`\\n\\nFinal Answer:\\n\\n${answer}`);\n    console.log('\\nAnswer has been saved to answer.md');\n  }\n\n  rl.close();\n}\n\nrun().catch(console.error);\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"esModuleInterop\": true,\n    \"incremental\": false,\n    \"isolatedModules\": true,\n    \"lib\": [\"es2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"moduleResolution\": \"Bundler\",\n    \"noUncheckedIndexedAccess\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"target\": \"ES2022\"\n  }\n}\n"
  }
]