From 5e05f1f56b6781c8e25d594a5cbc337ab3e64505 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi Date: Wed, 15 Oct 2025 23:39:42 +0530 Subject: [PATCH] fix: deep search --- apps/webapp/app/routes/api.v1.deep-search.tsx | 4 +- .../trigger/deep-search/deep-search-utils.ts | 61 +++++++++------ apps/webapp/app/trigger/deep-search/index.ts | 76 ++++++------------- apps/webapp/app/trigger/deep-search/utils.ts | 8 +- apps/webapp/app/utils/mcp/memory.ts | 42 +++++----- apps/webapp/package.json | 4 +- 6 files changed, 87 insertions(+), 108 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.deep-search.tsx b/apps/webapp/app/routes/api.v1.deep-search.tsx index 92897cb..0ea57f7 100644 --- a/apps/webapp/app/routes/api.v1.deep-search.tsx +++ b/apps/webapp/app/routes/api.v1.deep-search.tsx @@ -29,7 +29,7 @@ const { action, loader } = createActionApiRoute( }, async ({ body, authentication }) => { let trigger; - if (body.stream) { + if (!body.stream) { trigger = await deepSearch.trigger({ content: body.content, userId: authentication.userId, @@ -58,7 +58,7 @@ const { action, loader } = createActionApiRoute( return json({ error: "Run failed" }); } - } + }, ); export { action, loader }; diff --git a/apps/webapp/app/trigger/deep-search/deep-search-utils.ts b/apps/webapp/app/trigger/deep-search/deep-search-utils.ts index 8d36bfc..8cb9b2d 100644 --- a/apps/webapp/app/trigger/deep-search/deep-search-utils.ts +++ b/apps/webapp/app/trigger/deep-search/deep-search-utils.ts @@ -3,7 +3,7 @@ import { logger } from "@trigger.dev/sdk/v3"; import { generate } from "./stream-utils"; import { processTag } from "../chat/stream-utils"; import { type AgentMessage, AgentMessageType, Message } from "../chat/types"; -import { TotalCost } from "../utils/types"; +import { type TotalCost } from "../utils/types"; /** * Run the deep search ReAct loop @@ -12,7 +12,7 @@ import { TotalCost } from "../utils/types"; */ export async function* run( initialMessages: CoreMessage[], - searchTool: any + searchTool: any, ): AsyncGenerator { let messages = [...initialMessages]; let completed = false; @@ -34,13 +34,20 @@ export async function* run( try { while (!completed && guardLoop < 50) { - logger.info(`ReAct loop iteration ${guardLoop}, searches: ${searchCount}`); + logger.info( + `ReAct loop iteration ${guardLoop}, searches: ${searchCount}`, + ); // Call LLM with current message history - const response = generate(messages, (event)=>{const usage = event.usage; - totalCost.inputTokens += usage.promptTokens; - totalCost.outputTokens += usage.completionTokens; - }, tools); + const response = generate( + messages, + (event) => { + const usage = event.usage; + totalCost.inputTokens += usage.promptTokens; + totalCost.outputTokens += usage.completionTokens; + }, + tools, + ); let totalMessage = ""; const toolCalls: any[] = []; @@ -74,7 +81,7 @@ export async function* run( start: AgentMessageType.MESSAGE_START, chunk: AgentMessageType.MESSAGE_CHUNK, end: AgentMessageType.MESSAGE_END, - } + }, ); } } @@ -83,14 +90,14 @@ export async function* run( // Check for final response if (totalMessage.includes("")) { const match = totalMessage.match( - /(.*?)<\/final_response>/s + /(.*?)<\/final_response>/s, ); if (match) { // Accept synthesis - completed completed = true; logger.info( - `Final synthesis accepted after ${searchCount} searches, ${totalEpisodesFound} unique episodes found` + `Final synthesis accepted after ${searchCount} searches, ${totalEpisodesFound} unique episodes found`, ); break; } @@ -104,7 +111,7 @@ export async function* run( yield Message("", AgentMessageType.SKILL_START); yield Message( `\nSearching memory: "${toolCall.args.query}"...\n`, - AgentMessageType.SKILL_CHUNK + AgentMessageType.SKILL_CHUNK, ); yield Message("", AgentMessageType.SKILL_END); } @@ -114,7 +121,7 @@ export async function* run( searchTool.execute(toolCall.args).then((result: any) => ({ toolCall, result, - })) + })), ); const searchResults = await Promise.all(searchPromises); @@ -165,20 +172,20 @@ export async function* run( }); logger.info( - `Search ${searchCount} completed: ${episodesInThisSearch} episodes (${uniqueNewEpisodes} new, ${totalEpisodesFound} unique total)` + `Search ${searchCount} completed: ${episodesInThisSearch} episodes (${uniqueNewEpisodes} new, ${totalEpisodesFound} unique total)`, ); } // If found no episodes and haven't exhausted search attempts, require more searches if (totalEpisodesFound === 0 && searchCount < 7) { logger.info( - `Agent attempted synthesis with 0 unique episodes after ${searchCount} searches - requiring more attempts` + `Agent attempted synthesis with 0 unique episodes after ${searchCount} searches - requiring more attempts`, ); yield Message("", AgentMessageType.SKILL_START); yield Message( `No relevant context found yet - trying different search angles...`, - AgentMessageType.SKILL_CHUNK + AgentMessageType.SKILL_CHUNK, ); yield Message("", AgentMessageType.SKILL_END); @@ -202,7 +209,7 @@ Continue with different search strategies (you can search up to 7-10 times total // Soft nudging after all searches executed (awareness, not commands) if (totalEpisodesFound >= 30 && searchCount >= 3) { logger.info( - `Nudging: ${totalEpisodesFound} unique episodes found - suggesting synthesis consideration` + `Nudging: ${totalEpisodesFound} unique episodes found - suggesting synthesis consideration`, ); messages.push({ @@ -211,7 +218,7 @@ Continue with different search strategies (you can search up to 7-10 times total }); } else if (totalEpisodesFound >= 15 && searchCount >= 5) { logger.info( - `Nudging: ${totalEpisodesFound} unique episodes after ${searchCount} searches - suggesting evaluation` + `Nudging: ${totalEpisodesFound} unique episodes after ${searchCount} searches - suggesting evaluation`, ); messages.push({ @@ -220,22 +227,23 @@ Continue with different search strategies (you can search up to 7-10 times total }); } else if (searchCount >= 7) { logger.info( - `Nudging: ${searchCount} searches completed with ${totalEpisodesFound} unique episodes` + `Nudging: ${searchCount} searches completed with ${totalEpisodesFound} unique episodes`, ); messages.push({ role: "system", content: `Search depth: You have performed ${searchCount} searches and found ${totalEpisodesFound} unique episodes. Consider whether additional searches would yield meaningfully different context, or if it's time to synthesize what you've discovered.`, }); - } if (searchCount >= 10) { + } + if (searchCount >= 10) { logger.info( - `Reached maximum search limit (10), forcing synthesis with ${totalEpisodesFound} unique episodes` + `Reached maximum search limit (10), forcing synthesis with ${totalEpisodesFound} unique episodes`, ); yield Message("", AgentMessageType.SKILL_START); yield Message( `Maximum searches reached - synthesizing results...`, - AgentMessageType.SKILL_CHUNK + AgentMessageType.SKILL_CHUNK, ); yield Message("", AgentMessageType.SKILL_END); @@ -247,7 +255,10 @@ Continue with different search strategies (you can search up to 7-10 times total } // Safety check - if no tool calls and no final response, something went wrong - if (toolCalls.length === 0 && !totalMessage.includes("")) { + if ( + toolCalls.length === 0 && + !totalMessage.includes("") + ) { logger.warn("Agent produced neither tool calls nor final response"); messages.push({ @@ -261,11 +272,13 @@ Continue with different search strategies (you can search up to 7-10 times total } if (!completed) { - logger.warn(`Loop ended without completion after ${guardLoop} iterations`); + logger.warn( + `Loop ended without completion after ${guardLoop} iterations`, + ); yield Message("", AgentMessageType.MESSAGE_START); yield Message( "Deep search did not complete - maximum iterations reached.", - AgentMessageType.MESSAGE_CHUNK + AgentMessageType.MESSAGE_CHUNK, ); yield Message("", AgentMessageType.MESSAGE_END); } diff --git a/apps/webapp/app/trigger/deep-search/index.ts b/apps/webapp/app/trigger/deep-search/index.ts index 8924c29..a8b61f4 100644 --- a/apps/webapp/app/trigger/deep-search/index.ts +++ b/apps/webapp/app/trigger/deep-search/index.ts @@ -16,13 +16,7 @@ export const deepSearch = task({ id: "deep-search", maxDuration: 3000, run: async (payload: DeepSearchPayload): Promise => { - const { - content, - userId, - stream, - metadata: meta, - intentOverride, - } = payload; + const { content, userId, stream, metadata: meta, intentOverride } = payload; const randomKeyName = `deepSearch_${nanoid(10)}`; @@ -55,59 +49,33 @@ export const deepSearch = task({ // Run the ReAct loop generator const llmResponse = run(initialMessages, searchTool); - if (stream) { - // Streaming mode: stream via metadata.stream like chat.ts does - // This makes all message types available to clients in real-time - const messageStream = await metadata.stream("messages", llmResponse); + // Streaming mode: stream via metadata.stream like chat.ts does + // This makes all message types available to clients in real-time + const messageStream = await metadata.stream("messages", llmResponse); - let synthesis = ""; + let synthesis = ""; - for await (const step of messageStream) { - // MESSAGE_CHUNK: Final synthesis - accumulate and stream - if (step.type === AgentMessageType.MESSAGE_CHUNK) { - synthesis += step.message; - } - - // STREAM_END: Loop completed - if (step.type === AgentMessageType.STREAM_END) { - break; - } + for await (const step of messageStream) { + // MESSAGE_CHUNK: Final synthesis - accumulate and stream + if (step.type === AgentMessageType.MESSAGE_CHUNK) { + synthesis += step.message; } - await deletePersonalAccessToken(pat?.id); - - // Clean up any remaining tags - synthesis = synthesis - .replace(//gi, "") - .replace(/<\/final_response>/gi, "") - .trim(); - - return { synthesis }; - } else { - // Non-streaming mode: consume generator without streaming - let synthesis = ""; - - for await (const step of llmResponse) { - if (step.type === AgentMessageType.MESSAGE_CHUNK) { - synthesis += step.message; - } - // Could also collect episodes from tool results if needed + // STREAM_END: Loop completed + if (step.type === AgentMessageType.STREAM_END) { + break; } - - await deletePersonalAccessToken(pat?.id); - - // Clean up any remaining tags - synthesis = synthesis - .replace(//gi, "") - .replace(/<\/final_response>/gi, "") - .trim(); - - // For non-streaming, we need to get episodes from search results - // Since we don't have direct access to search results in this flow, - // we'll return synthesis without episodes for now - // (episodes can be extracted from tool results if needed) - return { synthesis }; } + + await deletePersonalAccessToken(pat?.id); + + // Clean up any remaining tags + synthesis = synthesis + .replace(//gi, "") + .replace(/<\/final_response>/gi, "") + .trim(); + + return { synthesis }; } catch (error) { await deletePersonalAccessToken(pat?.id); logger.error(`Deep search error: ${error}`); diff --git a/apps/webapp/app/trigger/deep-search/utils.ts b/apps/webapp/app/trigger/deep-search/utils.ts index f2a0cbd..75148c8 100644 --- a/apps/webapp/app/trigger/deep-search/utils.ts +++ b/apps/webapp/app/trigger/deep-search/utils.ts @@ -11,7 +11,7 @@ export function createSearchMemoryTool(token: string) { query: z .string() .describe( - "Search query to find relevant information. Be specific: entity names, topics, concepts." + "Search query to find relevant information. Be specific: entity names, topics, concepts.", ), }), execute: async ({ query }) => { @@ -23,7 +23,7 @@ export function createSearchMemoryTool(token: string) { headers: { Authorization: `Bearer ${token}`, }, - } + }, ); const searchResult = response.data; @@ -57,9 +57,7 @@ export function extractEpisodesFromToolCalls(toolCalls: any[]): any[] { // Deduplicate by content + createdAt const uniqueEpisodes = Array.from( - new Map( - episodes.map((e) => [`${e.content}-${e.createdAt}`, e]) - ).values() + new Map(episodes.map((e) => [`${e.content}-${e.createdAt}`, e])).values(), ); return uniqueEpisodes.slice(0, 10); diff --git a/apps/webapp/app/utils/mcp/memory.ts b/apps/webapp/app/utils/mcp/memory.ts index 77c5735..c4cdd90 100644 --- a/apps/webapp/app/utils/mcp/memory.ts +++ b/apps/webapp/app/utils/mcp/memory.ts @@ -179,27 +179,27 @@ export const memoryTools = [ required: ["integrationSlug", "action"], }, }, - { - name: "memory_deep_search", - description: - "Search CORE memory with document context and get synthesized insights. Automatically analyzes content to infer intent (reading, writing, meeting prep, research, task tracking, etc.) and provides context-aware synthesis. USE THIS TOOL: When analyzing documents, emails, notes, or any substantial text content for relevant memories. HOW TO USE: Provide the full content text. The tool will decompose it, search for relevant memories, and synthesize findings based on inferred intent. Returns: Synthesized context summary and related episodes.", - inputSchema: { - type: "object", - properties: { - content: { - type: "string", - description: - "Full document/text content to analyze and search against memory", - }, - intentOverride: { - type: "string", - description: - "Optional: Explicitly specify intent (e.g., 'meeting preparation', 'blog writing') instead of auto-detection", - }, - }, - required: ["content"], - }, - }, + // { + // name: "memory_deep_search", + // description: + // "Search CORE memory with document context and get synthesized insights. Automatically analyzes content to infer intent (reading, writing, meeting prep, research, task tracking, etc.) and provides context-aware synthesis. USE THIS TOOL: When analyzing documents, emails, notes, or any substantial text content for relevant memories. HOW TO USE: Provide the full content text. The tool will decompose it, search for relevant memories, and synthesize findings based on inferred intent. Returns: Synthesized context summary and related episodes.", + // inputSchema: { + // type: "object", + // properties: { + // content: { + // type: "string", + // description: + // "Full document/text content to analyze and search against memory", + // }, + // intentOverride: { + // type: "string", + // description: + // "Optional: Explicitly specify intent (e.g., 'meeting preparation', 'blog writing') instead of auto-detection", + // }, + // }, + // required: ["content"], + // }, + // }, ]; // Function to call memory tools based on toolName diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 2aec46d..292cd3b 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -10,8 +10,8 @@ "lint:fix": "eslint 'app/**/*.{ts,tsx,js,jsx}' --rule 'turbo/no-undeclared-env-vars:error' -f table", "start": "node server.js", "typecheck": "tsc", - "trigger:dev": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 dev", - "trigger:deploy": "pnpm dlx trigger.dev@4.0.0-v4-beta.22 deploy" + "trigger:dev": "pnpm dlx trigger.dev@4.0.4 dev", + "trigger:deploy": "pnpm dlx trigger.dev@4.0.4 deploy" }, "dependencies": { "@ai-sdk/amazon-bedrock": "2.2.12",