mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 08:48:29 +00:00
fix: deep search
This commit is contained in:
parent
949b534383
commit
5e05f1f56b
@ -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 };
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user