From 41691b380aebe44b035e04bc9f2181827353e7e6 Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 9 Apr 2025 10:06:29 +1000 Subject: [PATCH 01/15] docs: Add BYO deep research guide --- docs/guides/deno/byo-deep-research.mdx | 629 +++++++++++++++++++++++++ 1 file changed, 629 insertions(+) create mode 100644 docs/guides/deno/byo-deep-research.mdx diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx new file mode 100644 index 000000000..2fc1bd283 --- /dev/null +++ b/docs/guides/deno/byo-deep-research.mdx @@ -0,0 +1,629 @@ +--- +title_seo: 'Building Your Own Deep Research System with Nitric' +description: 'Learn how to build a simple research system using Nitric that can be tested locally and deployed to production' +author: tim_holm +languages: + - typescript +tags: + - API + - AI +published_at: 2025-04-10 +--- + +# Building a Simple Deep Research System with Nitric and Deno + +TLDR; You can check the example code [here](https://github.com/nitrictech/deep-research-example) + +In the era of large language models, building and testing research systems can be challenging due to API costs and rate limits. However, with the right approach, you can build a simple research system that can be tested locally using smaller models and then deployed to production with your preferred LLM provider. In this post, we'll explore how to create such a system using TypeScript and Nitric. + +## Why Local Testing? + +Before diving into the implementation, let's understand why local testing is valuable: + +1. **Rapid Development**: Test and iterate quickly without waiting for API responses +2. **Cost Control**: Avoid unnecessary API costs during development +3. **Reliability**: Test without worrying about API rate limits or availability +4. **Privacy**: Work with sensitive data without sending it to external services +5. **Flexibility**: Easily switch between local and production models +6. **Quick Iteration**: Test the entire pipeline without API costs +7. **Prompt Development**: Iterate quickly on prompts and logic using smaller models + +## The Architecture + +Our research system will be built as a Nitric API with several key components: + +1. **Search Engine Integration** +2. **Configurable LLM Interface** +3. **Topic-based Research Pipeline** +4. **Content Processing** +5. **REST API Endpoints** + +Let's break down each component: + +### 1. Search Engine Integration + +The search engine integration handles the "Web Search" step of the research pipeline: + +- Query Creation: Generate search queries from topics +- Web Search: Find relevant content +- Content Processing: Clean and convert content + +```typescript title:utils/search.ts +import { search, SafeSearchType } from 'duck-duck-scrape' + +export default async (query: string) => { + const results = await search(query, { safeSearch: SafeSearchType.STRICT }) + + // Get the top 3 most relevant results + const topResults = results.results.slice(0, 3) + + // Fetch HTML content for each result + const htmlContents = await Promise.all( + topResults.map(async (result) => { + try { + const response = await fetch(result.url) + const html = await response.text() + return { + url: result.url, + title: result.title, + html, + } + } catch (error) { + console.error(`Failed to fetch ${result.url}:`, error) + return null + } + }), + ) + + // Filter out failed fetches and return the results + return htmlContents.filter((content) => content !== null) +} +``` + +### 2. Configurable LLM Integration + +The LLM integration handles the "Summarization" and "Reflection" steps: + +- Summarization: Condense findings +- Reflection: Identify knowledge gaps +- Iteration: Continue research as needed + +The system can be configured to use different LLM providers through environment variables: + +```typescript title:services/api.ts +import openai from 'npm:openai' + +// Configuration from environment variables +const LLM_CONFIG = { + baseURL: Deno.env.get('LLM_BASE_URL') || 'http://localhost:11434/v1', + apiKey: Deno.env.get('LLM_API_KEY') || 'ollama', + model: Deno.env.get('LLM_MODEL') || 'llama3.2:3b', +} + +const OAI = new openai({ + baseURL: LLM_CONFIG.baseURL, + apiKey: LLM_CONFIG.apiKey, +}) + +// Example .env file: +// LLM_BASE_URL=http://localhost:11434/v1 # For local testing with Ollama +// LLM_BASE_URL=https://api.openai.com/v1 # For production with OpenAI +// LLM_API_KEY=your-api-key # For local this can be any value +// LLM_MODEL=gpt-4-turbo-preview # For production (or any other model of your choosing) +// LLM_MODEL=llama3.2:3b # For local testing +``` + +### Prompt Templates + +The system uses three types of LLM calls, each with its own prompt template: + +1. **Query Generation** (`queryPrompt`): Generates search queries based on the research topic + +```typescript title:prompts/query.ts +export default (date: string, topic: string) => ` +Your goal is to generate a targeted web search query. + + +Current date: ${date} +Please ensure your queries account for the most current information available as of this date. + + + +${topic} + + + +Format your response as a JSON object with ALL two of these exact keys: + - "query": The actual search query string + - "rationale": Brief explanation of why this query is relevant + + + +Example output: +{ + "query": "machine learning transformer architecture explained", + "rationale": "Understanding the fundamental structure of transformer models" +} + + +Provide your response only in JSON format: +` +``` + +2. **Content Summarization** (`summarizerPrompt`): Summarizes content in the context of research topics + +```typescript title:prompts/summarizer.ts +export default (topics: string[]) => ` +Generate a high-quality summary of the provided context for the following topics: +${topics.map((topic) => `- ${topic}`).join('\n')} + + + +When creating a NEW summary: +1. Highlight the most relevant information related to the user topic from the search results +2. Ensure a coherent flow of information + +When EXTENDING an existing summary: +1. Read the existing summary and new search results carefully. +2. Compare the new information with the existing summary. +3. For each piece of new information: + a. If it's related to existing points, integrate it into the relevant paragraph. + b. If it's entirely new but relevant, add a new paragraph with a smooth transition. + c. If it's not relevant to the user topic, skip it. +4. Ensure all additions are relevant to the user's topic. +5. Verify that your final output differs from the input summary. + + + +- Start directly with the updated summary, without preamble or titles. Do not use XML tags in the output. + + + +Think carefully about the provided Context first. Then generate a summary of the context to address the User Input. +` +``` + +3. **Research Reflection** (`reflectionPrompt`): Analyzes current research to identify knowledge gaps + +```typescript title:prompts/reflect.ts +export default (topics: string[]) => ` +You are an expert research assistant analyzing a summary about the following topics: +${topics.map((topic) => `- ${topic}`).join('\n')}. + + +1. If the provided context is enough to answer the questions, do not generate a follow-up query. +2. Identify knowledge gaps or areas that need deeper exploration +3. Be careful not to generate follow-up queries that are not related to the topics. +4. Generate a follow-up question that would help expand your understanding + + + +Ensure the follow-up question is self-contained and includes necessary context for web search. + + +Provide only the follow-up query in your response, if there are no follow-up queries, respond with nothing. +` +``` + +These prompts work together to create a research system that: + +- Generate search queries from topics +- Find relevant content using the duckduckgo search API +- Cleans and converts content to simple markdown +- Summarizes findings +- Attempts to identify knowledge gaps +- Continues research if required + +### 3. Topic-based Research Pipeline + +The system uses a topic-based approach to conduct research, with different message types for each stage of the process: + +```typescript title:services/api.ts +type TopicMessageTypes = 'create_query' | 'query' | 'reflect' | 'summarize' + +interface ResearchTopicMessage { + type: T + topics: string[] + summaries: string[] + remainingIterations: number +} + +interface CreateQueryTopicMessage extends ResearchTopicMessage<'create_query'> { + type: 'create_query' + date: string + originalTopic: string +} + +interface PerformQueryTopicMessage extends ResearchTopicMessage<'query'> { + type: 'query' + query: { + query: string + rationale: string + } +} + +interface SummarizeTopicMessage extends ResearchTopicMessage<'summarize'> { + type: 'summarize' + content: string +} + +interface ReflectTopicMessage extends ResearchTopicMessage<'reflect'> { + type: 'reflect' + content: string +} +``` + +### 4. Content Processing + +The system includes HTML cleaning and markdown conversion: + +```typescript title:services/api.ts +function cleanHtml(html: string): string { + const $ = cheerio.load(html) + + // Remove script and style tags + $('script, style, noscript, iframe, embed, object').remove() + + // Remove navigation elements + $('nav, header, footer, aside, .nav, .navigation, .menu, .sidebar').remove() + + // Remove ads and social media elements + $('.ad, .ads, .advertisement, .social, .share, .comments').remove() + + // Remove empty elements + $('*').each(function () { + if ($(this).text().trim() === '') { + $(this).remove() + } + }) + + // Get the main content + let content = $('article, main, .article, .content, .post, .entry') + if (content.length === 0) { + content = $('body') + } + + return content.html() || '' +} +``` + +### 5. The Nitric API Implementation + +The main API implementation ties everything together: + +```typescript title:services/api.ts +import { api, topic, bucket } from 'npm:@nitric/sdk' +import search from '../utils/search.ts' +import openai from 'npm:openai' +import queryPrompt from '../prompts/query.ts' +import summarizerPrompt from '../prompts/summarizer.ts' +import reflectionPrompt from '../prompts/reflect.ts' + +const OAI = new openai({ + baseURL: 'http://localhost:11434/v1', + apiKey: 'ollama', +}) + +const researchApi = api('research') +const researchTopic = topic('research') +const researchTopicPub = researchTopic.allow('publish') +const researchBucket = bucket('research').allow('write') + +const MODEL = 'llama3.2:3b' +const MAX_ITERATIONS = 3 + +// Subscribe to the research topic to handle different types of messages +researchTopic.subscribe(async (ctx) => { + const message = ctx.req.json() + + switch (message.type) { + case 'create_query': + await handleCreateQuery(message) + break + case 'query': + await handleQuery(message) + break + case 'summarize': + await handleSummarize(message) + break + case 'reflect': + await handleReflect(message) + break + default: + console.error(`Unknown message type: ${message.type}`) + } +}) + +// Implementation of message handlers +async function handleCreateQuery(message: CreateQueryTopicMessage) { + console.log( + `[Research] Starting new query for topic: ${message.originalTopic}`, + ) + + // Create a new query using ollama + const completion = await OAI.chat.completions.create({ + model: MODEL, + messages: [ + { + role: 'system', + content: queryPrompt(new Date().toISOString(), message.originalTopic), + }, + { role: 'user', content: message.originalTopic }, + ], + }) + + console.log( + `[Research] Generated query: ${completion.choices[0].message.content}`, + ) + + // Parse the JSON response to extract query and rationale + const response = JSON.parse(completion.choices[0].message.content!) + const { query, rationale } = response + + console.log(`[Research] Query: ${query}\nRationale: ${rationale}`) + + // Submit a new query creation request to the research service + await researchTopicPub.publish({ + ...message, + type: 'query', + topics: [...message.topics, query], + query: { + query, + rationale, + }, + }) +} + +async function handleQuery(message: PerformQueryTopicMessage) { + console.log(`[Research] Executing search query: ${message.query.query}`) + + // Perform the given query using our search + const result = await search(message.query.query) + + console.log(`[Research] Found ${result.length} results`) + console.log(`[Research] First result preview:`, { + url: result[0]?.url, + title: result[0]?.title, + html: result[0]?.html.substring(0, 200) + '...', + }) + + // Clean and convert HTML to markdown + console.log( + `[Research] Converting HTML to Markdown for URL: ${result[0]?.url}`, + ) + console.log(`[Research] Original HTML length: ${result[0]?.html.length}`) + + const cleanedHtml = cleanHtml(result[0]?.html) + console.log(`[Research] Cleaned HTML length: ${cleanedHtml.length}`) + + const markdown = turndownService.turndown(cleanedHtml) + console.log( + `[Research] Converted HTML to Markdown: ${markdown.substring(0, 200)}...`, + ) + console.log(`[Research] Markdown content length: ${markdown.length}`) + + // Publish a summarize request to the research service + await researchTopicPub.publish({ + ...message, + type: 'summarize', + content: markdown, + }) +} + +async function handleSummarize(message: SummarizeTopicMessage) { + console.log( + `[Research] Summarizing content for topic: ${message.topics[message.topics.length - 1]}`, + ) + + // Append message content to previous summaries + const summaries = [...message.summaries, message.content] + + console.log( + `[Research] Previous summaries count: ${message.summaries.length}`, + ) + console.log(`[Research] Current content length: ${message.content.length}`) + + // Reduce summaries into a single string + const fullSummary = summaries.join('\n') + console.log(`[Research] Combined summary length: ${fullSummary.length}`) + + // Summarize the given content + const completion = await OAI.chat.completions.create({ + model: MODEL, + messages: [ + { role: 'system', content: summarizerPrompt(message.topics) }, + { role: 'user', content: fullSummary }, + ], + }) + + const summary = completion.choices[0].message.content! + console.log(`[Research] Generated summary: ${summary}`) + + // Publish a reflect request to the research service + await researchTopicPub.publish({ + summaries: [ + // Reset to the newly compacted summary + summary, + ], + remainingIterations: message.remainingIterations, + topics: [...message.topics, message.topics[message.topics.length - 1]], + type: 'reflect', + content: summary, + }) +} + +async function handleReflect(message: ReflectTopicMessage) { + console.log(`[Research] Reflecting on summary for topics: ${message.topics}`) + console.log( + `[Research] Current summary: ${message.content.substring(0, 200)}...`, + ) + console.log(`[Research] Remaining iterations: ${message.remainingIterations}`) + + // Check iteration limit + if (message.remainingIterations <= 0) { + console.log( + `[Research] No iterations remaining. Writing final summary to bucket.`, + ) + // Combine all summaries with clear topic separation + const finalSummary = message.summaries + .map( + (summary, index) => + `## Research Topic: ${message.topics[index]}\n${summary}`, + ) + .join('\n\n') + + await researchBucket.file(message.topics[0]).write(finalSummary) + return + } + + // Use the reflection prompt to restart the research chain + const completion = await OAI.chat.completions.create({ + model: MODEL, + messages: [ + { role: 'system', content: reflectionPrompt(message.topics) }, + { role: 'user', content: message.content }, + ], + }) + + console.log( + `[Research] Parsing reflection:`, + completion.choices[0].message.content!, + ) + + const reflection = completion.choices[0].message.content! + + // Only restart research if knowledge gaps were identified + if (reflection !== '') { + console.log( + `[Research] Found knowledge gap, following up with: ${reflection}`, + ) + await researchTopicPub.publish({ + ...message, + remainingIterations: message.remainingIterations - 1, + type: 'create_query', + topics: [...message.topics], + originalTopic: reflection, + date: new Date().toISOString(), + }) + } else { + console.log( + `[Research] No knowledge gaps found. Writing final summary to bucket: ${message.topics[message.topics.length - 1]}`, + ) + // Combine all summaries with clear topic separation + const finalSummary = message.summaries + .map( + (summary, index) => + `## Research Topic: ${message.topics[index]}\n${summary}`, + ) + .join('\n\n') + + console.log(`[Research] Final summary length: ${finalSummary.length}`) + await researchBucket.file(message.topics[0]).write(finalSummary) + } +} + +// API endpoint to start research +researchApi.post('/query', async (ctx) => { + const query = ctx.req.text() + const remainingIterations = MAX_ITERATIONS + + // Submit off start of research chain + await researchTopicPub.publish({ + summaries: [], + remainingIterations, + type: 'create_query', + date: new Date().toISOString(), + topics: [], + originalTopic: query, + }) + + ctx.res.body = 'Query submitted' + return ctx +}) +``` + +## Configuration Options + +This app can be configured through environment variables: + +```bash +# Local Development example (using Ollama) +LLM_BASE_URL=http://localhost:11434/v1 +LLM_API_KEY=ollama +LLM_MODEL=llama3.2:3b + +# Production Example (using OpenAI) +LLM_BASE_URL=https://api.openai.com/v1 +LLM_API_KEY=your-openai-key +LLM_MODEL=gpt-4-turbo-preview + +# Other Options +MAX_ITERATIONS=3 +SEARCH_RESULTS=3 +``` + +Production deployment is as simple as updating these environment variables: + +- Switch to production models by updating the base URL and API key +- Use more powerful models for better results +- Scale the system as needed + +## Testing it locally + +To test the system locally: + +1. **Install and Start Ollama**: + + First, [install Ollama](https://ollama.ai/) for your operating system. + + Then pull and start the model: + + ```bash + ollama pull llama2:3b + ollama serve + ``` + +2. **Configure Environment**: + + Create a `.env` file with local testing configuration: + + ```bash + LLM_BASE_URL=http://localhost:11434/v1 + LLM_API_KEY=ollama + LLM_MODEL=llama2:3b + MAX_ITERATIONS=3 + SEARCH_RESULTS=3 + ``` + +3. **Start the Local Development Server**: + + ```bash + nitric start + ``` + +4. **Test the API**: + + Send a POST request to start research: + + ```bash + curl -X POST http://localhost:4001/query \ + -H "Content-Type: text/plain" \ + -d "quantum computing basics" + ``` + + The system will begin its research process, and you can monitor the progress in the Nitric development server logs. + +Note: Local testing with smaller models may produce different results compared to production models, but the workflow and functionality will remain the same. This allows you to iterate quickly on your implementation without incurring API costs. + +## Conclusion + +Building a research system that can be tested locally and deployed to production gives you the best of both worlds. You can develop and test quickly using local models, then deploy to production with your preferred LLM provider. The system we've built here is a starting point that you can extend and customize based on your specific needs. + +Remember that the quality of your research will depend on: + +- The quality of your source documents +- The capabilities of your chosen LLM +- The effectiveness of your document processing pipeline +- The relevance of your search and retrieval system + +With these components in place, you're well on your way to building a simple research assistant that can be developed locally and deployed to production with ease. From b2e16c432d6430fa6a37e1e1f95d696c59e47a5f Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 9 Apr 2025 11:39:03 +1000 Subject: [PATCH 02/15] update dictionary --- dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dictionary.txt b/dictionary.txt index 702cfac93..a1743521f 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -267,6 +267,7 @@ DNSSEC AzureTF VSCode https +duckduckgo [0-9]+px ^.+[-:_]\w+$ [a-z]+([A-Z0-9]|[A-Z0-9]\w+) From 9326ef0f415029c8f1e86bb66f22e116ece47483 Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 9 Apr 2025 11:44:11 +1000 Subject: [PATCH 03/15] update code --- docs/guides/deno/byo-deep-research.mdx | 86 ++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 2fc1bd283..219dff394 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -28,6 +28,63 @@ Before diving into the implementation, let's understand why local testing is val 6. **Quick Iteration**: Test the entire pipeline without API costs 7. **Prompt Development**: Iterate quickly on prompts and logic using smaller models +## Project Setup and Dependencies + +Before we start implementing our research system, let's set up the project and install the necessary dependencies: + +1. **Create a new Nitric project**: + + ```bash + # Install Nitric CLI if you haven't already + npm install -g @nitric/cli + + # Create a new Nitric project + nitric new deep-research + cd deep-research + ``` + +2. **Configure dependencies in deno.json**: + + Create or update the `deno.json` file in your project root: + + ```json title:deno.json + { + "imports": { + "@nitric/sdk": "npm:@nitric/sdk", + "openai": "npm:openai", + "duck-duck-scrape": "npm:duck-duck-scrape", + "cheerio": "npm:cheerio", + "turndown": "npm:turndown" + }, + "tasks": { + "start": "deno run --allow-net --allow-env --allow-read main.ts" + } + } + ``` + +3. **Install dependencies**: + + ```bash + # Install dependencies using Deno + deno install + ``` + +4. **Project structure**: + + ``` + deep-research/ + ├── deno.json # Deno configuration and dependencies + ├── .env # Environment variables + ├── utils/ + │ └── search.ts # Search functionality + ├── prompts/ + │ ├── query.ts # Query generation prompt + │ ├── summarizer.ts # Content summarization prompt + │ └── reflect.ts # Research reflection prompt + └── services/ + └── api.ts # Main API implementation + ``` + ## The Architecture Our research system will be built as a Nitric API with several key components: @@ -91,7 +148,7 @@ The LLM integration handles the "Summarization" and "Reflection" steps: The system can be configured to use different LLM providers through environment variables: ```typescript title:services/api.ts -import openai from 'npm:openai' +import { OpenAI } from 'openai' // Configuration from environment variables const LLM_CONFIG = { @@ -100,7 +157,7 @@ const LLM_CONFIG = { model: Deno.env.get('LLM_MODEL') || 'llama3.2:3b', } -const OAI = new openai({ +const OAI = new OpenAI({ baseURL: LLM_CONFIG.baseURL, apiKey: LLM_CONFIG.apiKey, }) @@ -258,8 +315,17 @@ interface ReflectTopicMessage extends ResearchTopicMessage<'reflect'> { The system includes HTML cleaning and markdown conversion: ```typescript title:services/api.ts +import { load as cheerioLoad } from 'cheerio' +import { TurndownService } from 'turndown' + +// Initialize TurndownService for HTML to Markdown conversion +const turndownService = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', +}) + function cleanHtml(html: string): string { - const $ = cheerio.load(html) + const $ = cheerioLoad(html) // Remove script and style tags $('script, style, noscript, iframe, embed, object').remove() @@ -292,14 +358,22 @@ function cleanHtml(html: string): string { The main API implementation ties everything together: ```typescript title:services/api.ts -import { api, topic, bucket } from 'npm:@nitric/sdk' +import { api, topic, bucket } from '@nitric/sdk' import search from '../utils/search.ts' -import openai from 'npm:openai' +import { OpenAI } from 'openai' import queryPrompt from '../prompts/query.ts' import summarizerPrompt from '../prompts/summarizer.ts' import reflectionPrompt from '../prompts/reflect.ts' +import { load as cheerioLoad } from 'cheerio' +import { TurndownService } from 'turndown' + +// Initialize TurndownService for HTML to Markdown conversion +const turndownService = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', +}) -const OAI = new openai({ +const OAI = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama', }) From 823f3ff2f2b784e691fbd9c24be04823a4e872d5 Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 9 Apr 2025 12:05:04 +1000 Subject: [PATCH 04/15] fix install instructions. --- docs/guides/deno/byo-deep-research.mdx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 219dff394..391354a18 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -34,14 +34,14 @@ Before we start implementing our research system, let's set up the project and i 1. **Create a new Nitric project**: - ```bash - # Install Nitric CLI if you haven't already - npm install -g @nitric/cli +If you haven't already install Nitric CLI by following the [official installation guide](https://nitric-docs-git-docs-byo-deep-research-nitrictech.vercel.app/docs/get-started/installation) - # Create a new Nitric project - nitric new deep-research - cd deep-research - ``` +Then create a new Nitric project with: + +```bash +nitric new deep-research +cd deep-research +``` 2. **Configure dependencies in deno.json**: @@ -646,7 +646,7 @@ Production deployment is as simple as updating these environment variables: To test the system locally: -1. **Install and Start Ollama**: +1. **Install and Start Ollama (optional)**: First, [install Ollama](https://ollama.ai/) for your operating system. @@ -657,6 +657,8 @@ To test the system locally: ollama serve ``` + > You can skip this step if you want to use OpenAI or other hosted solution as your LLM provider. + 2. **Configure Environment**: Create a `.env` file with local testing configuration: From 9aed649634fdba593a0cbb6092dd81f513fd7772 Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 9 Apr 2025 12:16:46 +1000 Subject: [PATCH 05/15] add additional notes --- docs/guides/deno/byo-deep-research.mdx | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 391354a18..4089f9d85 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -71,19 +71,23 @@ cd deep-research 4. **Project structure**: - ``` - deep-research/ - ├── deno.json # Deno configuration and dependencies - ├── .env # Environment variables - ├── utils/ - │ └── search.ts # Search functionality - ├── prompts/ - │ ├── query.ts # Query generation prompt - │ ├── summarizer.ts # Content summarization prompt - │ └── reflect.ts # Research reflection prompt - └── services/ - └── api.ts # Main API implementation - ``` +The following is the project structure for this example (feel free to create these files in advance): + +``` +deep-research/ +├── deno.json # Deno configuration and dependencies +├── .env # Environment variables +├── utils/ +│ └── search.ts # Search functionality +├── prompts/ +│ ├── query.ts # Query generation prompt +│ ├── summarizer.ts # Content summarization prompt +│ └── reflect.ts # Research reflection prompt +└── services/ + └── api.ts # Main API implementation +``` + +> Code snippets presented will have the name of the file they are in included in the title. ## The Architecture From 59311a74c80b3d9be068789446709cbb7414d17f Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 9 Apr 2025 13:04:16 +1000 Subject: [PATCH 06/15] resync code examples. --- docs/guides/deno/byo-deep-research.mdx | 117 +++++++++++++++---------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 4089f9d85..1f283ec60 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -454,8 +454,7 @@ async function handleCreateQuery(message: CreateQueryTopicMessage) { async function handleQuery(message: PerformQueryTopicMessage) { console.log(`[Research] Executing search query: ${message.query.query}`) - - // Perform the given query using our search + // perform the given query using our search const result = await search(message.query.query) console.log(`[Research] Found ${result.length} results`) @@ -465,26 +464,20 @@ async function handleQuery(message: PerformQueryTopicMessage) { html: result[0]?.html.substring(0, 200) + '...', }) - // Clean and convert HTML to markdown - console.log( - `[Research] Converting HTML to Markdown for URL: ${result[0]?.url}`, - ) - console.log(`[Research] Original HTML length: ${result[0]?.html.length}`) - - const cleanedHtml = cleanHtml(result[0]?.html) - console.log(`[Research] Cleaned HTML length: ${cleanedHtml.length}`) + const content = result.reduce((acc, curr) => { + const cleanedContent = cleanHtml(curr.html) + return `${acc} - const markdown = turndownService.turndown(cleanedHtml) - console.log( - `[Research] Converted HTML to Markdown: ${markdown.substring(0, 200)}...`, - ) - console.log(`[Research] Markdown content length: ${markdown.length}`) + # ${curr.title} + ${turndownService.turndown(cleanedContent)} +` + }, '') - // Publish a summarize request to the research service + // publish a summarize request to the research service await researchTopicPub.publish({ ...message, type: 'summarize', - content: markdown, + content: content, }) } @@ -493,34 +486,31 @@ async function handleSummarize(message: SummarizeTopicMessage) { `[Research] Summarizing content for topic: ${message.topics[message.topics.length - 1]}`, ) - // Append message content to previous summaries - const summaries = [...message.summaries, message.content] - console.log( `[Research] Previous summaries count: ${message.summaries.length}`, ) console.log(`[Research] Current content length: ${message.content.length}`) - // Reduce summaries into a single string - const fullSummary = summaries.join('\n') - console.log(`[Research] Combined summary length: ${fullSummary.length}`) - - // Summarize the given content + // Process the current content in isolation const completion = await OAI.chat.completions.create({ model: MODEL, messages: [ - { role: 'system', content: summarizerPrompt(message.topics) }, - { role: 'user', content: fullSummary }, + { + role: 'system', + content: summarizerPrompt([message.topics[message.topics.length - 1]]), + }, + { role: 'user', content: message.content }, ], }) const summary = completion.choices[0].message.content! console.log(`[Research] Generated summary: ${summary}`) - // Publish a reflect request to the research service + // publish a reflect request to the research service await researchTopicPub.publish({ summaries: [ - // Reset to the newly compacted summary + // Keep all previous summaries and add the new one + ...message.summaries, summary, ], remainingIterations: message.remainingIterations, @@ -542,19 +532,29 @@ async function handleReflect(message: ReflectTopicMessage) { console.log( `[Research] No iterations remaining. Writing final summary to bucket.`, ) - // Combine all summaries with clear topic separation - const finalSummary = message.summaries - .map( - (summary, index) => - `## Research Topic: ${message.topics[index]}\n${summary}`, - ) - .join('\n\n') + // Create a more comprehensive final summary with proper structure + const finalSummary = `# Research Summary: ${message.topics[0]} + +## Introduction +This document contains research findings on the topic "${message.topics[0]}". The research was conducted through multiple iterations of querying, analyzing, and synthesizing information. + +## Research Findings +${message.summaries + .map( + (summary, index) => + `### Research Topic: ${message.topics[index]}\n\n${summary}`, + ) + .join('\n\n')} + +## Conclusion +This research provides a comprehensive overview of "${message.topics[0]}" and related topics. The findings are based on multiple sources and have been synthesized to provide a coherent understanding of the subject matter. +` await researchBucket.file(message.topics[0]).write(finalSummary) return } - // Use the reflection prompt to restart the research chain + // Here I want to use the reflection prompt I have to restart the research chain const completion = await OAI.chat.completions.create({ model: MODEL, messages: [ @@ -587,19 +587,48 @@ async function handleReflect(message: ReflectTopicMessage) { console.log( `[Research] No knowledge gaps found. Writing final summary to bucket: ${message.topics[message.topics.length - 1]}`, ) - // Combine all summaries with clear topic separation - const finalSummary = message.summaries - .map( - (summary, index) => - `## Research Topic: ${message.topics[index]}\n${summary}`, - ) - .join('\n\n') + // Create a more comprehensive final summary with proper structure + const finalSummary = `# Research Summary: ${message.topics[0]} + +## Introduction +This document contains research findings on the topic "${message.topics[0]}". The research was conducted through multiple iterations of querying, analyzing, and synthesizing information. + +## Research Findings +${message.summaries + .map( + (summary, index) => + `### Research Topic: ${message.topics[index]}\n\n${summary}`, + ) + .join('\n\n')} + +## Conclusion +This research provides a comprehensive overview of "${message.topics[0]}" and related topics. The findings are based on multiple sources and have been synthesized to provide a coherent understanding of the subject matter. +` console.log(`[Research] Final summary length: ${finalSummary.length}`) await researchBucket.file(message.topics[0]).write(finalSummary) } } +researchApi.post('/query', async (ctx) => { + const query = ctx.req.text() + const remainingIterations = MAX_ITERATIONS + + // Submit off start of research chain + await researchTopicPub.publish({ + summaries: [], + remainingIterations, + type: 'create_query', + date: new Date().toISOString(), + topics: [], + originalTopic: query, + }) + + ctx.res.body = 'Query submitted' + + return ctx +}) + // API endpoint to start research researchApi.post('/query', async (ctx) => { const query = ctx.req.text() From 044b21f11a2c4943d71d4bec7bb8bdb47331256b Mon Sep 17 00:00:00 2001 From: David Moore <4121492+davemooreuws@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:14:40 +1000 Subject: [PATCH 07/15] Update date --- docs/guides/deno/byo-deep-research.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 1f283ec60..4681384e7 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -7,7 +7,7 @@ languages: tags: - API - AI -published_at: 2025-04-10 +published_at: 2025-04-29 --- # Building a Simple Deep Research System with Nitric and Deno From 0a9b99246caf6ab7e3a0af0ba7999bc1a7075c23 Mon Sep 17 00:00:00 2001 From: David Moore <4121492+davemooreuws@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:31:19 +1000 Subject: [PATCH 08/15] update AI tag to match others --- docs/guides/deno/byo-deep-research.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 4681384e7..d17a53662 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -6,7 +6,7 @@ languages: - typescript tags: - API - - AI + - AI & Machine Learning published_at: 2025-04-29 --- From 36927e2828cfbed4699cd2e44079ff65398d1154 Mon Sep 17 00:00:00 2001 From: David Moore <4121492+davemooreuws@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:29:38 +1000 Subject: [PATCH 09/15] add deno template to command --- docs/guides/deno/byo-deep-research.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index d17a53662..08b4ee4bc 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -39,7 +39,7 @@ If you haven't already install Nitric CLI by following the [official installatio Then create a new Nitric project with: ```bash -nitric new deep-research +nitric new deep-research ts-starter-deno cd deep-research ``` From d464db28d20716e3cb457182e73d13b58881241f Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 30 Apr 2025 13:28:32 +1000 Subject: [PATCH 10/15] Apply suggestions from code review Co-authored-by: Ryan Cartwright <39504851+HomelessDinosaur@users.noreply.github.com> --- docs/guides/deno/byo-deep-research.mdx | 102 ++++++++++++------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 08b4ee4bc..1ab4a9e59 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -32,9 +32,9 @@ Before diving into the implementation, let's understand why local testing is val Before we start implementing our research system, let's set up the project and install the necessary dependencies: -1. **Create a new Nitric project**: +### 1. **Create a new Nitric project**: -If you haven't already install Nitric CLI by following the [official installation guide](https://nitric-docs-git-docs-byo-deep-research-nitrictech.vercel.app/docs/get-started/installation) +If you haven't already, install Nitric CLI by following the [official installation guide](https://nitric-docs-git-docs-byo-deep-research-nitrictech.vercel.app/docs/get-started/installation) Then create a new Nitric project with: @@ -45,29 +45,29 @@ cd deep-research 2. **Configure dependencies in deno.json**: - Create or update the `deno.json` file in your project root: - - ```json title:deno.json - { - "imports": { - "@nitric/sdk": "npm:@nitric/sdk", - "openai": "npm:openai", - "duck-duck-scrape": "npm:duck-duck-scrape", - "cheerio": "npm:cheerio", - "turndown": "npm:turndown" - }, - "tasks": { - "start": "deno run --allow-net --allow-env --allow-read main.ts" - } - } - ``` +Create or update the `deno.json` file in your project root: + +```json title:deno.json +{ + "imports": { + "@nitric/sdk": "npm:@nitric/sdk", + "openai": "npm:openai", + "duck-duck-scrape": "npm:duck-duck-scrape", + "cheerio": "npm:cheerio", + "turndown": "npm:turndown" + }, + "tasks": { + "start": "deno run --allow-net --allow-env --allow-read main.ts" + } +} +``` 3. **Install dependencies**: - ```bash - # Install dependencies using Deno - deno install - ``` +```bash +# Install dependencies using Deno +deno install +``` 4. **Project structure**: @@ -143,7 +143,7 @@ export default async (query: string) => { ### 2. Configurable LLM Integration -The LLM integration handles the "Summarization" and "Reflection" steps: +The LLM integration handles the "Summarization", "Reflection", and "Iteration" steps: - Summarization: Condense findings - Reflection: Identify knowledge gaps @@ -268,8 +268,8 @@ Provide only the follow-up query in your response, if there are no follow-up que These prompts work together to create a research system that: -- Generate search queries from topics -- Find relevant content using the duckduckgo search API +- Generates search queries from topics +- Finds relevant content using the duckduckgo search API - Cleans and converts content to simple markdown - Summarizes findings - Attempts to identify knowledge gaps @@ -681,48 +681,48 @@ To test the system locally: 1. **Install and Start Ollama (optional)**: - First, [install Ollama](https://ollama.ai/) for your operating system. +First, [install Ollama](https://ollama.ai/) for your operating system. - Then pull and start the model: +Then pull and start the model: - ```bash - ollama pull llama2:3b - ollama serve - ``` +```bash +ollama pull llama2:3b +ollama serve +``` - > You can skip this step if you want to use OpenAI or other hosted solution as your LLM provider. +> You can skip this step if you want to use OpenAI or other hosted solution as your LLM provider. 2. **Configure Environment**: - Create a `.env` file with local testing configuration: +Create a `.env` file with local testing configuration: - ```bash - LLM_BASE_URL=http://localhost:11434/v1 - LLM_API_KEY=ollama - LLM_MODEL=llama2:3b - MAX_ITERATIONS=3 - SEARCH_RESULTS=3 - ``` +```bash +LLM_BASE_URL=http://localhost:11434/v1 +LLM_API_KEY=ollama +LLM_MODEL=llama2:3b +MAX_ITERATIONS=3 +SEARCH_RESULTS=3 +``` 3. **Start the Local Development Server**: - ```bash - nitric start - ``` +```bash +nitric start +``` 4. **Test the API**: - Send a POST request to start research: +Send a POST request to start research: - ```bash - curl -X POST http://localhost:4001/query \ - -H "Content-Type: text/plain" \ - -d "quantum computing basics" - ``` +```bash +curl -X POST http://localhost:4001/query \ + -H "Content-Type: text/plain" \ + -d "quantum computing basics" +``` - The system will begin its research process, and you can monitor the progress in the Nitric development server logs. +The system will begin its research process, and you can monitor the progress in the Nitric development server logs. -Note: Local testing with smaller models may produce different results compared to production models, but the workflow and functionality will remain the same. This allows you to iterate quickly on your implementation without incurring API costs. +Local testing with smaller models may produce different results compared to production models, but the workflow and functionality will remain the same. This allows you to iterate quickly on your implementation without incurring API costs. ## Conclusion From 196bf2aeb19f7888e6e016bde7f8d336ed8e8121 Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 30 Apr 2025 14:05:37 +1000 Subject: [PATCH 11/15] apply additional suggestions. --- docs/guides/deno/byo-deep-research.mdx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 1ab4a9e59..468b96d8e 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -87,8 +87,6 @@ deep-research/ └── api.ts # Main API implementation ``` -> Code snippets presented will have the name of the file they are in included in the title. - ## The Architecture Our research system will be built as a Nitric API with several key components: @@ -730,9 +728,7 @@ Building a research system that can be tested locally and deployed to production Remember that the quality of your research will depend on: -- The quality of your source documents -- The capabilities of your chosen LLM -- The effectiveness of your document processing pipeline -- The relevance of your search and retrieval system +- The quality of information in your source documents +- The size and power of your chosen LLM With these components in place, you're well on your way to building a simple research assistant that can be developed locally and deployed to production with ease. From f420076d5bfc076348823d1810b5639a7a079103 Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 30 Apr 2025 14:08:27 +1000 Subject: [PATCH 12/15] apply heading suggestions. --- docs/guides/deno/byo-deep-research.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 468b96d8e..fbe57ac0c 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -172,11 +172,11 @@ const OAI = new OpenAI({ // LLM_MODEL=llama3.2:3b # For local testing ``` -### Prompt Templates +### 3. Prompt Templates The system uses three types of LLM calls, each with its own prompt template: -1. **Query Generation** (`queryPrompt`): Generates search queries based on the research topic +#### 1. **Query Generation** (`queryPrompt`): Generates search queries based on the research topic ```typescript title:prompts/query.ts export default (date: string, topic: string) => ` @@ -209,7 +209,7 @@ Provide your response only in JSON format: ` ``` -2. **Content Summarization** (`summarizerPrompt`): Summarizes content in the context of research topics +#### 2. **Content Summarization** (`summarizerPrompt`): Summarizes content in the context of research topics ```typescript title:prompts/summarizer.ts export default (topics: string[]) => ` @@ -237,12 +237,12 @@ When EXTENDING an existing summary: - Start directly with the updated summary, without preamble or titles. Do not use XML tags in the output. - + Think carefully about the provided Context first. Then generate a summary of the context to address the User Input. -` +` ``` -3. **Research Reflection** (`reflectionPrompt`): Analyzes current research to identify knowledge gaps +#### 3. **Research Reflection** (`reflectionPrompt`): Analyzes current research to identify knowledge gaps ```typescript title:prompts/reflect.ts export default (topics: string[]) => ` @@ -273,7 +273,7 @@ These prompts work together to create a research system that: - Attempts to identify knowledge gaps - Continues research if required -### 3. Topic-based Research Pipeline +### 4. Topic-based Research Pipeline The system uses a topic-based approach to conduct research, with different message types for each stage of the process: @@ -312,7 +312,7 @@ interface ReflectTopicMessage extends ResearchTopicMessage<'reflect'> { } ``` -### 4. Content Processing +### 5. Content Processing The system includes HTML cleaning and markdown conversion: @@ -355,7 +355,7 @@ function cleanHtml(html: string): string { } ``` -### 5. The Nitric API Implementation +### 6. The Nitric API Implementation The main API implementation ties everything together: From e0969eca8c49ec293adaad27434387d674d52f8d Mon Sep 17 00:00:00 2001 From: Tim Holm Date: Wed, 30 Apr 2025 14:14:09 +1000 Subject: [PATCH 13/15] include links to cheerio and turndown. --- docs/guides/deno/byo-deep-research.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index fbe57ac0c..ea2499d8d 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -314,7 +314,7 @@ interface ReflectTopicMessage extends ResearchTopicMessage<'reflect'> { ### 5. Content Processing -The system includes HTML cleaning and markdown conversion: +We'll also include [turndown](https://github.com/domchristie/turndown) and [cheerio](https://github.com/cheeriojs/cheerio) to clean the HTML content and convert it to markdown. To be more easily interpretable by the LLM. ```typescript title:services/api.ts import { load as cheerioLoad } from 'cheerio' From eb7477764dfe5e5dc2c0753878de90acd588b756 Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Wed, 30 Apr 2025 15:34:23 +1000 Subject: [PATCH 14/15] Update docs/guides/deno/byo-deep-research.mdx --- docs/guides/deno/byo-deep-research.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index ea2499d8d..1f8f4c59d 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -688,7 +688,9 @@ ollama pull llama2:3b ollama serve ``` -> You can skip this step if you want to use OpenAI or other hosted solution as your LLM provider. + + You can skip this step if you want to use OpenAI or other hosted solution as your LLM provider. + 2. **Configure Environment**: From 2f346dd6396180ec39d8e9da24ff945aadaa7d04 Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Wed, 30 Apr 2025 15:39:29 +1000 Subject: [PATCH 15/15] spelling fixes --- dictionary.txt | 1 + docs/guides/deno/byo-deep-research.mdx | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/dictionary.txt b/dictionary.txt index a1743521f..3a9ecf816 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -262,6 +262,7 @@ VMs json KMS CDN +turndown subdirectories DNSSEC AzureTF diff --git a/docs/guides/deno/byo-deep-research.mdx b/docs/guides/deno/byo-deep-research.mdx index 1f8f4c59d..31791da21 100644 --- a/docs/guides/deno/byo-deep-research.mdx +++ b/docs/guides/deno/byo-deep-research.mdx @@ -314,7 +314,7 @@ interface ReflectTopicMessage extends ResearchTopicMessage<'reflect'> { ### 5. Content Processing -We'll also include [turndown](https://github.com/domchristie/turndown) and [cheerio](https://github.com/cheeriojs/cheerio) to clean the HTML content and convert it to markdown. To be more easily interpretable by the LLM. +We'll also include [turndown](https://github.com/domchristie/turndown) and [cheerio](https://github.com/cheeriojs/cheerio) to clean the HTML content and convert it to markdown. Making it easier to interpret by the LLM. ```typescript title:services/api.ts import { load as cheerioLoad } from 'cheerio' @@ -689,7 +689,8 @@ ollama serve ``` - You can skip this step if you want to use OpenAI or other hosted solution as your LLM provider. + You can skip this step if you want to use OpenAI or other hosted solution as + your LLM provider. 2. **Configure Environment**: @@ -722,7 +723,12 @@ curl -X POST http://localhost:4001/query \ The system will begin its research process, and you can monitor the progress in the Nitric development server logs. -Local testing with smaller models may produce different results compared to production models, but the workflow and functionality will remain the same. This allows you to iterate quickly on your implementation without incurring API costs. + + Local testing with smaller models may produce different results compared to + production models, but the workflow and functionality will remain the same. + This allows you to iterate quickly on your implementation without incurring + API costs. + ## Conclusion