fix: deep search

This commit is contained in:
Harshith Mullapudi 2025-10-15 23:39:42 +05:30
parent 949b534383
commit 5e05f1f56b
6 changed files with 87 additions and 108 deletions

View File

@ -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 };

View File

@ -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<AgentMessage, any, any> {
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("<final_response>")) {
const match = totalMessage.match(
/<final_response>(.*?)<\/final_response>/s
/<final_response>(.*?)<\/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("<final_response>")) {
if (
toolCalls.length === 0 &&
!totalMessage.includes("<final_response>")
) {
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);
}

View File

@ -16,13 +16,7 @@ export const deepSearch = task({
id: "deep-search",
maxDuration: 3000,
run: async (payload: DeepSearchPayload): Promise<DeepSearchResponse> => {
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(/<final_response>/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(/<final_response>/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(/<final_response>/gi, "")
.replace(/<\/final_response>/gi, "")
.trim();
return { synthesis };
} catch (error) {
await deletePersonalAccessToken(pat?.id);
logger.error(`Deep search error: ${error}`);

View File

@ -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);

View File

@ -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

View File

@ -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",