diff --git a/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts b/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts index dc840cbc..40586f5b 100644 --- a/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts +++ b/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts @@ -6,6 +6,7 @@ import { import { createGetURLContentTool, createShellTool, + createSearchDocumentForTool, } from "../../../../tools/index.js"; import { PlannerGraphState, @@ -79,7 +80,8 @@ export async function generateAction( createSearchTool(state), createShellTool(state), createPlannerNotesTool(), - createGetURLContentTool(), + createGetURLContentTool(state), + createSearchDocumentForTool(state, config), ...mcpTools, ]; logger.info( diff --git a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts index 17ecea7f..ca992baa 100644 --- a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts +++ b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts @@ -245,6 +245,7 @@ export async function interruptProposedPlan( targetRepository: state.targetRepository, githubIssueId: state.githubIssueId, internalMessages: state.messages, + documentCache: state.documentCache, }; if (state.autoAcceptPlan) { diff --git a/apps/open-swe/src/graphs/planner/nodes/take-action.ts b/apps/open-swe/src/graphs/planner/nodes/take-action.ts index 900634ce..412c9298 100644 --- a/apps/open-swe/src/graphs/planner/nodes/take-action.ts +++ b/apps/open-swe/src/graphs/planner/nodes/take-action.ts @@ -7,6 +7,7 @@ import { import { createGetURLContentTool, createShellTool, + createSearchDocumentForTool, } from "../../../tools/index.js"; import { GraphConfig } from "@open-swe/shared/open-swe/types"; import { @@ -18,7 +19,7 @@ import { safeSchemaToString, safeBadArgsError, } from "../../../utils/zod-to-string.js"; -import { truncateOutput } from "../../../utils/truncate-outputs.js"; + import { createSearchTool } from "../../../tools/search.js"; import { getChangedFilesStatus, @@ -32,6 +33,7 @@ import { shouldDiagnoseError } from "../../../utils/tool-message-error.js"; import { Command } from "@langchain/langgraph"; import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js"; import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants"; +import { processToolCallContent } from "../../../utils/tool-output-processing.js"; const logger = createLogger(LogLevel.INFO, "TakeAction"); @@ -49,14 +51,22 @@ export async function takeActions( const shellTool = createShellTool(state); const searchTool = createSearchTool(state); const plannerNotesTool = createPlannerNotesTool(); - const getURLContentTool = createGetURLContentTool(); + const getURLContentTool = createGetURLContentTool(state); + const searchDocumentForTool = createSearchDocumentForTool(state, config); const mcpTools = await getMcpTools(config); + const higherContextLimitToolNames = [ + ...mcpTools.map((t) => t.name), + getURLContentTool.name, + searchDocumentForTool.name, + ]; + const allTools = [ shellTool, searchTool, plannerNotesTool, getURLContentTool, + searchDocumentForTool, ...mcpTools, ]; const toolsMap = Object.fromEntries( @@ -88,7 +98,7 @@ export async function takeActions( status: "error", }); - return toolMessage; + return { toolMessage, stateUpdates: undefined }; } logger.info("Executing planner tool action", { @@ -142,26 +152,46 @@ export async function takeActions( } } - const truncatedOutput = - toolCall.name === getURLContentTool.name - ? // Allow for more context to be included from URL contents. - truncateOutput(result, { - numStartCharacters: 10000, - numEndCharacters: 10000, - }) - : truncateOutput(result); + const { content, stateUpdates } = await processToolCallContent( + toolCall, + result, + { + higherContextLimitToolNames, + state, + config, + }, + ); const toolMessage = new ToolMessage({ id: uuidv4(), tool_call_id: toolCall.id ?? "", - content: truncatedOutput, + content, name: toolCall.name, status: toolCallStatus, }); - return toolMessage; + + return { toolMessage, stateUpdates }; }); - let toolCallResults = await Promise.all(toolCallResultsPromise); + const toolCallResultsWithUpdates = await Promise.all(toolCallResultsPromise); + let toolCallResults = toolCallResultsWithUpdates.map( + (item) => item.toolMessage, + ); + + // merging document cache updates from tool calls + const allStateUpdates = toolCallResultsWithUpdates + .map((item) => item.stateUpdates) + .filter(Boolean) + .reduce( + (acc: { documentCache: Record }, update) => { + if (update?.documentCache) { + acc.documentCache = { ...acc.documentCache, ...update.documentCache }; + } + return acc; + }, + { documentCache: {} } as { documentCache: Record }, + ); + const repoPath = getRepoAbsolutePath(state.targetRepository); const changedFiles = await getChangedFilesStatus(repoPath, sandbox); if (changedFiles?.length > 0) { @@ -201,6 +231,7 @@ ${tc.content}`, sandboxSessionId: sandbox.id, ...(codebaseTree && { codebaseTree }), ...(dependenciesInstalled !== null && { dependenciesInstalled }), + ...allStateUpdates, }; const maxContextActions = config.configurable?.maxContextActions ?? 75; diff --git a/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts b/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts index 98df8f57..50428af5 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts @@ -15,6 +15,7 @@ import { createRequestHumanHelpToolFields, createUpdatePlanToolFields, createGetURLContentTool, + createSearchDocumentForTool, } from "../../../../tools/index.js"; import { formatPlanPrompt } from "../../../../utils/plan-prompt.js"; import { stopSandbox } from "../../../../utils/sandbox.js"; @@ -153,9 +154,10 @@ export async function generateAction( createApplyPatchTool(state), createRequestHumanHelpToolFields(), createUpdatePlanToolFields(), - createGetURLContentTool(), + createGetURLContentTool(state), createInstallDependenciesTool(state), markTaskCompletedTool, + createSearchDocumentForTool(state, config), ...mcpTools, ]; logger.info( diff --git a/apps/open-swe/src/graphs/programmer/nodes/take-action.ts b/apps/open-swe/src/graphs/programmer/nodes/take-action.ts index 556a6f9e..55a3dab8 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/take-action.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/take-action.ts @@ -5,6 +5,7 @@ import { createApplyPatchTool, createGetURLContentTool, createShellTool, + createSearchDocumentForTool, } from "../../../tools/index.js"; import { GraphState, @@ -20,7 +21,7 @@ import { safeBadArgsError, } from "../../../utils/zod-to-string.js"; import { Command } from "@langchain/langgraph"; -import { truncateOutput } from "../../../utils/truncate-outputs.js"; + import { getSandboxWithErrorHandling } from "../../../utils/sandbox.js"; import { FAILED_TO_GENERATE_TREE_MESSAGE, @@ -32,6 +33,7 @@ import { createSearchTool } from "../../../tools/search.js"; import { getMcpTools } from "../../../utils/mcp-client.js"; import { shouldDiagnoseError } from "../../../utils/tool-message-error.js"; import { getGitHubTokensFromConfig } from "../../../utils/github-tokens.js"; +import { processToolCallContent } from "../../../utils/tool-output-processing.js"; const logger = createLogger(LogLevel.INFO, "TakeAction"); @@ -49,16 +51,23 @@ export async function takeAction( const shellTool = createShellTool(state); const searchTool = createSearchTool(state); const installDependenciesTool = createInstallDependenciesTool(state); - const getURLContentTool = createGetURLContentTool(); - + const getURLContentTool = createGetURLContentTool(state); + const searchDocumentForTool = createSearchDocumentForTool(state, config); const mcpTools = await getMcpTools(config); + const higherContextLimitToolNames = [ + ...mcpTools.map((t) => t.name), + getURLContentTool.name, + searchDocumentForTool.name, + ]; + const allTools = [ shellTool, searchTool, installDependenciesTool, applyPatchTool, getURLContentTool, + searchDocumentForTool, ...mcpTools, ]; const toolsMap = Object.fromEntries( @@ -89,7 +98,7 @@ export async function takeAction( name: toolCall.name, status: "error", }); - return toolMessage; + return { toolMessage, stateUpdates: undefined }; } let result = ""; @@ -136,18 +145,45 @@ export async function takeAction( } } + const { content, stateUpdates } = await processToolCallContent( + toolCall, + result, + { + higherContextLimitToolNames, + state, + config, + }, + ); + const toolMessage = new ToolMessage({ id: uuidv4(), tool_call_id: toolCall.id ?? "", - content: truncateOutput(result), + content, name: toolCall.name, status: toolCallStatus, }); - return toolMessage; + return { toolMessage, stateUpdates }; }); - const toolCallResults = await Promise.all(toolCallResultsPromise); + const toolCallResultsWithUpdates = await Promise.all(toolCallResultsPromise); + const toolCallResults = toolCallResultsWithUpdates.map( + (item) => item.toolMessage, + ); + + // merging document cache updates from tool calls + const allStateUpdates = toolCallResultsWithUpdates + .map((item) => item.stateUpdates) + .filter(Boolean) + .reduce( + (acc: { documentCache: Record }, update) => { + if (update?.documentCache) { + acc.documentCache = { ...acc.documentCache, ...update.documentCache }; + } + return acc; + }, + { documentCache: {} } as { documentCache: Record }, + ); let wereDependenciesInstalled: boolean | null = null; toolCallResults.forEach((toolCallResult) => { @@ -209,6 +245,7 @@ export async function takeAction( ...(dependenciesInstalledUpdate !== null && { dependenciesInstalled: dependenciesInstalledUpdate, }), + ...allStateUpdates, }; return new Command({ goto: shouldRouteDiagnoseNode ? "diagnose-error" : "generate-action", diff --git a/apps/open-swe/src/tools/index.ts b/apps/open-swe/src/tools/index.ts index c7d45258..03c4d11a 100644 --- a/apps/open-swe/src/tools/index.ts +++ b/apps/open-swe/src/tools/index.ts @@ -1,6 +1,7 @@ export * from "./apply-patch.js"; export * from "./shell.js"; export * from "./url-content.js"; +export * from "./search-documents-for/index.js"; export { createUpdatePlanToolFields, createSessionPlanToolFields, diff --git a/apps/open-swe/src/tools/search-documents-for/index.ts b/apps/open-swe/src/tools/search-documents-for/index.ts new file mode 100644 index 00000000..2d43e98e --- /dev/null +++ b/apps/open-swe/src/tools/search-documents-for/index.ts @@ -0,0 +1,124 @@ +import { tool } from "@langchain/core/tools"; +import { createLogger, LogLevel } from "../../utils/logger.js"; +import { createSearchDocumentForToolFields } from "@open-swe/shared/open-swe/tools"; +import { FireCrawlLoader } from "@langchain/community/document_loaders/web/firecrawl"; +import { loadModel, Task } from "../../utils/load-model.js"; +import { GraphConfig, GraphState } from "@open-swe/shared/open-swe/types"; +import { getMessageContentString } from "@open-swe/shared/messages"; +import { DOCUMENT_SEARCH_PROMPT } from "./prompt.js"; +import { parseUrl } from "../../utils/url-parser.js"; +import { z } from "zod"; + +const logger = createLogger(LogLevel.INFO, "SearchDocumentForTool"); + +type SearchDocumentForInput = z.infer< + ReturnType["schema"] +>; + +export function createSearchDocumentForTool( + state: Pick, + config: GraphConfig, +) { + const searchDocumentForTool = tool( + async ( + input: SearchDocumentForInput, + ): Promise<{ + result: string; + status: "success" | "error"; + stateUpdates?: Partial>; + }> => { + const { url, query } = input; + + const urlParseResult = parseUrl(url); + if (!urlParseResult.success) { + return { result: urlParseResult.errorMessage, status: "error" }; + } + const parsedUrl = urlParseResult.url?.href; + + try { + let documentContent = state.documentCache[parsedUrl]; + + if (!documentContent) { + logger.info("Document not cached, fetching via FireCrawl", { + url: parsedUrl, + }); + const loader = new FireCrawlLoader({ + url: parsedUrl, + mode: "scrape", + params: { + formats: ["markdown"], + }, + }); + + const docs = await loader.load(); + documentContent = docs.map((doc) => doc.pageContent).join("\n\n"); + + if (state.documentCache) { + const stateUpdates = { + documentCache: { + ...state.documentCache, + [parsedUrl]: documentContent, + }, + }; + return { result: documentContent, status: "success", stateUpdates }; + } + } else { + logger.info("Using cached document content", { + url: parsedUrl, + contentLength: documentContent.length, + }); + } + + if (!documentContent.trim()) { + return { + result: `No content found at URL: ${url}`, + status: "error", + }; + } + + const model = await loadModel(config, Task.SUMMARIZER); + + const searchPrompt = DOCUMENT_SEARCH_PROMPT.replace( + "{DOCUMENT_PAGE_CONTENT}", + documentContent, + ).replace("{NATURAL_LANGUAGE_QUERY}", query); + + const response = await model + .withConfig({ tags: ["nostream"], runName: "document-search" }) + .invoke([ + { + role: "user", + content: searchPrompt, + }, + ]); + + const searchResult = getMessageContentString(response.content); + + logger.info("Document search completed", { + url, + query, + resultLength: searchResult.length, + }); + + return { + result: searchResult, + status: "success", + }; + } catch (e) { + const errorString = e instanceof Error ? e.message : String(e); + logger.error("Failed to search document", { + url: parsedUrl, + query, + error: errorString, + }); + return { + result: `Failed to search document at ${parsedUrl}\nError:\n${errorString}`, + status: "error", + }; + } + }, + createSearchDocumentForToolFields(), + ); + + return searchDocumentForTool; +} diff --git a/apps/open-swe/src/tools/search-documents-for/prompt.ts b/apps/open-swe/src/tools/search-documents-for/prompt.ts new file mode 100644 index 00000000..e55bf0c3 --- /dev/null +++ b/apps/open-swe/src/tools/search-documents-for/prompt.ts @@ -0,0 +1,59 @@ +export const DOCUMENT_SEARCH_PROMPT = ` +You are a specialized document information extraction agent. Your sole purpose is to find and extract relevant information from web documents and documentation based on natural language queries. You are precise, thorough, and never add information not present in the source. + + + +Document Search Agent - Information Extraction Phase + + + +Extract ALL information from the provided document that relates to the natural language query. Preserve code snippets, URLs, file paths, and references exactly as they appear in the source document. + + + + + - **Extract Only What Exists**: Only extract information that is explicitly present in the document. NEVER add, infer, assume, or generate any information not directly found in the source material. + - **Comprehensive Coverage**: Scan the entire document for any content related to the query, including direct mentions and relevant examples or context. + - **Exact Preservation**: Copy all code snippets, file paths, URLs, and technical content exactly as written. Maintain original formatting, indentation, and structure. + - **No Hallucination**: Do not create, modify, or infer any information. If something is not in the document, do not include it. + - **Context Inclusion**: When extracting text, include enough surrounding context to make the information meaningful. + + + + Your response must use this exact structure: + + + + [All prose, explanations, and descriptions from the document that relate to the query. Preserve original wording and include sufficient context.] + + + + [All code blocks and technical examples related to the query. Use markdown code blocks with language tags. Preserve exact formatting.] + + + + [All URLs, file paths, import statements, and references found. Format as: + - URLs: "Display Text: [URL]" or "[URL]" + - Paths: "Path: [path/to/file]" + - Imports: "Import: [statement]" + - Packages: "Package: [name]"] + + + + + + - Only extract content that actually exists in the provided document + - Never add explanations, interpretations, or additional context not present in the source + - If no relevant information is found, leave sections empty but still include them + - Preserve all technical details exactly as written + + + + +{NATURAL_LANGUAGE_QUERY} + + + +{DOCUMENT_PAGE_CONTENT} + +`; diff --git a/apps/open-swe/src/tools/url-content.ts b/apps/open-swe/src/tools/url-content.ts index f4aaca04..e2153b8a 100644 --- a/apps/open-swe/src/tools/url-content.ts +++ b/apps/open-swe/src/tools/url-content.ts @@ -2,46 +2,83 @@ import { tool } from "@langchain/core/tools"; import { createLogger, LogLevel } from "../utils/logger.js"; import { createGetURLContentToolFields } from "@open-swe/shared/open-swe/tools"; import { FireCrawlLoader } from "@langchain/community/document_loaders/web/firecrawl"; +import { GraphState } from "@open-swe/shared/open-swe/types"; +import { parseUrl } from "../utils/url-parser.js"; const logger = createLogger(LogLevel.INFO, "GetURLContentTool"); -export function createGetURLContentTool() { +export function createGetURLContentTool( + state: Pick, +) { const getURLContentTool = tool( - async (input): Promise<{ result: string; status: "success" | "error" }> => { + async ( + input, + ): Promise<{ + result: string; + status: "success" | "error"; + stateUpdates?: Partial>; + }> => { const { url } = input; - let parsedUrl: URL | null = null; - try { - parsedUrl = new URL(url); - } catch (e) { - const errorString = e instanceof Error ? e.message : String(e); - logger.error("Failed to parse URL", { url, error: errorString }); - return { - result: `Failed to parse URL: ${url}\nError:\n${errorString}\nPlease ensure the URL provided is properly formatted.`, - status: "error", - }; + + const urlParseResult = parseUrl(url); + if (!urlParseResult.success) { + return { result: urlParseResult.errorMessage, status: "error" }; } + const parsedUrl = urlParseResult.url?.href; try { - const loader = new FireCrawlLoader({ - url: parsedUrl.href, - mode: "scrape", - params: { - formats: ["markdown"], - }, - }); + let documentContent = state.documentCache[parsedUrl]; - const docs = await loader.load(); - const text = docs.map((doc) => doc.pageContent).join("\n\n"); + if (!documentContent) { + logger.info("Document not cached, fetching via FireCrawl", { + url: parsedUrl, + }); + const loader = new FireCrawlLoader({ + url: parsedUrl, + mode: "scrape", + params: { + formats: ["markdown"], + }, + }); + + const docs = await loader.load(); + documentContent = docs.map((doc) => doc.pageContent).join("\n\n"); + + if (state.documentCache) { + const stateUpdates = { + documentCache: { + ...state.documentCache, + [parsedUrl]: documentContent, + }, + }; + return { result: documentContent, status: "success", stateUpdates }; + } + } else { + logger.info("Using cached document content", { + url: parsedUrl, + contentLength: documentContent.length, + }); + } + + if (!documentContent.trim()) { + return { + result: `No content found at URL: ${url}`, + status: "error", + }; + } return { - result: text, + result: documentContent, status: "success", }; } catch (e) { const errorString = e instanceof Error ? e.message : String(e); - logger.error("Failed to get URL content", { url, error: errorString }); + logger.error("Failed to get URL content", { + url: parsedUrl, + error: errorString, + }); return { - result: `Failed to get URL content: ${url}\nError:\n${errorString}`, + result: `Failed to get URL content: ${parsedUrl}\nError:\n${errorString}`, status: "error", }; } diff --git a/apps/open-swe/src/utils/load-model.ts b/apps/open-swe/src/utils/load-model.ts index 581c8e6f..401e8ee4 100644 --- a/apps/open-swe/src/utils/load-model.ts +++ b/apps/open-swe/src/utils/load-model.ts @@ -31,7 +31,7 @@ const TASK_TO_CONFIG_DEFAULTS_MAP = { temperature: 0, }, [Task.SUMMARIZER]: { - modelName: "anthropic:claude-sonnet-4-0", + modelName: "google-genai:gemini-2.5-pro", temperature: 0, }, }; diff --git a/apps/open-swe/src/utils/mcp-output/index.ts b/apps/open-swe/src/utils/mcp-output/index.ts new file mode 100644 index 00000000..24c19a78 --- /dev/null +++ b/apps/open-swe/src/utils/mcp-output/index.ts @@ -0,0 +1,96 @@ +import { GraphConfig } from "@open-swe/shared/open-swe/types"; +import { loadModel, Task } from "../load-model.js"; +import { createLogger, LogLevel } from "../logger.js"; +import { DOCUMENT_TOC_GENERATION_PROMPT } from "./prompt.js"; +import { getMessageContentString } from "@open-swe/shared/messages"; +import { truncateOutput } from "../truncate-outputs.js"; + +const logger = createLogger(LogLevel.INFO, "McpOutputHandler"); + +export async function handleMcpDocumentationOutput( + output: string, + config: GraphConfig, + options?: { + maxLength?: number; + url?: string; + }, +): Promise { + const { maxLength = 40000, url = "" } = options ?? {}; + + // If output is within limits, return as-is + if (output.length <= maxLength) { + return output; + } + + logger.info("MCP output exceeds max length, generating table of contents", { + outputLength: output.length, + maxLength, + url, + }); + + try { + const model = await loadModel(config, Task.SUMMARIZER); + + const systemPrompt = DOCUMENT_TOC_GENERATION_PROMPT.replace( + "{DOCUMENT_PAGE_CONTENT}", + output, + ); + + const response = await model + .withConfig({ tags: ["nostream"], runName: "mcp-doc-toc-generation" }) + .invoke([ + { + role: "user", + content: systemPrompt, + }, + ]); + + const tableOfContents = getMessageContentString(response.content); + + const explanatoryMessage = createExplanatoryMessage(url, tableOfContents); + return explanatoryMessage; + } catch (error) { + logger.error("Failed to generate MCP documentation summary", { + ...(error instanceof Error + ? { name: error.name, message: error.message, stack: error.stack } + : { error }), + }); + + return truncateOutput(output, { + numStartCharacters: 20000, + numEndCharacters: 20000, + }); + } +} + +function createExplanatoryMessage( + url: string, + tableOfContents: string, +): string { + const urlInfo = url ? ` from ${url}` : ""; + const searchInstruction = url + ? `To get specific information from this document, use: search_document_for("${url}", "your natural language query")` + : "To get specific information from this document, use the search_document_for tool with the url of the page and the natural language query"; + + return `The following output was truncated due to its length exceeding the maximum allowed characters. Content${urlInfo} received ${tableOfContents ? "exceeded" : "exceeds"} 40,000 characters. + +The following provides a table of contents of the document content: + +${tableOfContents || "Table of contents generation failed"} + +${searchInstruction}`; +} + +// Keep the original simple function for backwards compatibility +export function handleMcpOutput( + output: string, + options?: { + maxLength?: number; + }, +) { + const { maxLength = 40000 } = options ?? {}; + if (output.length > maxLength) { + return "swoosh!!"; + } + return output; +} diff --git a/apps/open-swe/src/utils/mcp-output/prompt.ts b/apps/open-swe/src/utils/mcp-output/prompt.ts new file mode 100644 index 00000000..e5991ae6 --- /dev/null +++ b/apps/open-swe/src/utils/mcp-output/prompt.ts @@ -0,0 +1,44 @@ +export const DOCUMENT_TOC_GENERATION_PROMPT = `You are a terminal-based agentic coding assistant built by LangChain. You excel at analyzing and structuring technical documentation with maximum comprehensiveness. + + +Generate a comprehensive table of contents with brief summaries for the provided documentation. Your goal is to capture EVERY concept, method, function, API, configuration option, and idea present in the document with maximum breadth coverage. + + + +1. **Maximum Breadth**: Include ALL headings, subheadings, and any mentioned concepts, methods, functions, APIs, or configuration options - leave nothing out +2. **Comprehensive Coverage**: Every idea, technique, or approach mentioned should appear in your table of contents, even if briefly discussed +3. **Minimum Viable Details**: For each item, provide the essential information needed to understand what it is and its purpose (1-2 sentences) +4. **Exact Hierarchy**: Preserve the document's structure while ensuring no content is overlooked +5. **Concept Extraction**: Beyond just headings, identify and list any significant concepts, methods, or functions discussed within sections +6. **No Omissions**: If a function, API endpoint, configuration parameter, or concept is mentioned anywhere, it should be represented +7. **CRITICAL - Preserve URLs/Paths**: NEVER remove or modify URLs, relative paths, file paths, or any link references. Include them exactly as they appear in the original document to maintain navigability and reference accuracy + + + +- Scan for ALL functions, methods, classes, and APIs mentioned +- Include configuration options, parameters, and settings discussed +- Capture examples, use cases, and implementation approaches +- Note any troubleshooting, limitations, or best practices mentioned +- List any related tools, libraries, or dependencies referenced +- **PRESERVE ALL URLs, file paths, and relative paths exactly as written** - do not modify, shorten, or remove any links or path references + + + +Wrap your response in XML tags and use Markdown list syntax with maximum detail coverage: + +\`\`\` + +- Main Section: Brief description covering the primary concepts and methods. + - Subsection: Specific functionality, APIs, or methods discussed here. + - Function/Method Name: What this specific function does and key parameters. + - Configuration Option: Purpose and usage of this setting. + - Concept/Approach: Brief explanation of this technique or idea. + - Another Subsection: Additional methods, concepts, or approaches. + - Related Functions: Any additional functions or utilities mentioned. + +\`\`\` + + + +{DOCUMENT_PAGE_CONTENT} +`; diff --git a/apps/open-swe/src/utils/model-manager.ts b/apps/open-swe/src/utils/model-manager.ts index 04625d47..c5a64bc7 100644 --- a/apps/open-swe/src/utils/model-manager.ts +++ b/apps/open-swe/src/utils/model-manager.ts @@ -254,7 +254,7 @@ export class ModelManager { [Task.SUMMARIZER]: { modelName: config.configurable?.[`${task}ModelName`] ?? - "anthropic:claude-sonnet-4-0", + "google-genai:gemini-2.5-pro", temperature: config.configurable?.[`${task}Temperature`] ?? 0, }, }; diff --git a/apps/open-swe/src/utils/tool-output-processing.ts b/apps/open-swe/src/utils/tool-output-processing.ts new file mode 100644 index 00000000..2ddceff8 --- /dev/null +++ b/apps/open-swe/src/utils/tool-output-processing.ts @@ -0,0 +1,67 @@ +import { GraphConfig, GraphState } from "@open-swe/shared/open-swe/types"; +import { truncateOutput } from "./truncate-outputs.js"; +import { handleMcpDocumentationOutput } from "./mcp-output/index.js"; +import { parseUrl } from "./url-parser.js"; + +interface ToolCall { + name: string; + args?: Record; +} + +/** + * Processes tool call results with appropriate content handling based on tool type. + * Handles search_document_for, MCP tools, and regular tools with different truncation strategies. + * Returns a new state object with the updated document cache if the tool is a higher context limit tool. + */ +export async function processToolCallContent( + toolCall: ToolCall, + result: string, + options: { + higherContextLimitToolNames: string[]; + state: Pick; + config: GraphConfig; + }, +): Promise<{ + content: string; + stateUpdates?: Partial>; +}> { + const { higherContextLimitToolNames, state, config } = options; + + if (toolCall.name === "search_document_for") { + return { + content: truncateOutput(result, { + numStartCharacters: 20000, + numEndCharacters: 20000, + }), + }; + } else if (higherContextLimitToolNames.includes(toolCall.name)) { + const url = toolCall.args?.url || toolCall.args?.uri || toolCall.args?.path; + const parsedResult = typeof url === "string" ? parseUrl(url) : null; + const parsedUrl = parsedResult?.success ? parsedResult.url.href : undefined; + const processedContent = await handleMcpDocumentationOutput( + result, + config, + { + url: parsedUrl, + }, + ); + + const stateUpdates = parsedUrl + ? { + documentCache: { + ...state.documentCache, + [parsedUrl]: result, + }, + } + : undefined; + + return { + content: processedContent, + stateUpdates, + }; + } else { + return { + content: truncateOutput(result), + }; + } +} diff --git a/apps/open-swe/src/utils/url-parser.ts b/apps/open-swe/src/utils/url-parser.ts new file mode 100644 index 00000000..07d7f9e7 --- /dev/null +++ b/apps/open-swe/src/utils/url-parser.ts @@ -0,0 +1,36 @@ +import { createLogger, LogLevel } from "./logger.js"; + +const logger = createLogger(LogLevel.INFO, "URLParser"); + +interface URLParseResult { + success: true; + url: URL; +} + +interface URLParseError { + success: false; + errorMessage: string; +} + +type URLParseResponse = URLParseResult | URLParseError; + +/** + * Safely parses a URL string and returns a structured result. + */ +export function parseUrl(urlString: string): URLParseResponse { + try { + const parsedUrl = new URL(urlString); + return { + success: true, + url: parsedUrl, + }; + } catch (e) { + const errorString = e instanceof Error ? e.message : String(e); + logger.error("Failed to parse URL", { url: urlString, error: errorString }); + + return { + success: false, + errorMessage: `Failed to parse URL: ${urlString}\nError:\n${errorString}.`, + }; + } +} diff --git a/apps/web/src/components/gen-ui/action-step.tsx b/apps/web/src/components/gen-ui/action-step.tsx index 4d5c700f..cdba07f7 100644 --- a/apps/web/src/components/gen-ui/action-step.tsx +++ b/apps/web/src/components/gen-ui/action-step.tsx @@ -24,6 +24,7 @@ import { createTakePlannerNotesFields, createGetURLContentToolFields, createSearchToolFields, + createSearchDocumentForToolFields, } from "@open-swe/shared/open-swe/tools"; import { z } from "zod"; import { @@ -51,6 +52,8 @@ const getURLContentTool = createGetURLContentToolFields(); type GetURLContentToolArgs = z.infer; const searchTool = createSearchToolFields(dummyRepo); type SearchToolArgs = z.infer; +const searchDocumentForTool = createSearchDocumentForToolFields(); +type SearchDocumentForToolArgs = z.infer; // Common props for all action types type BaseActionProps = { @@ -102,6 +105,12 @@ type SearchActionProps = BaseActionProps & errorCode?: number; }; +type SearchDocumentForActionProps = BaseActionProps & + Partial & { + actionType: "search_document_for"; + output?: string; + }; + type McpActionProps = BaseActionProps & { actionType: "mcp"; toolName: string; @@ -117,7 +126,8 @@ export type ActionItemProps = | PlannerNotesActionProps | GetURLContentActionProps | McpActionProps - | SearchActionProps; + | SearchActionProps + | SearchDocumentForActionProps; export type ActionStepProps = { actions: ActionItemProps[]; @@ -131,6 +141,7 @@ const ACTION_GENERATING_TEXT_MAP = { [installDependenciesTool.name]: "Installing dependencies...", [plannerNotesTool.name]: "Saving notes...", [getURLContentTool.name]: "Fetching URL content...", + [searchDocumentForTool.name]: "Searching document...", [searchTool.name]: "Searching...", }; @@ -207,6 +218,10 @@ function ActionItem(props: ActionItemProps) { return props.success ? "URL content fetched" : "Failed to fetch URL content"; + } else if (props.actionType === "search_document_for") { + return props.success + ? "Document search completed" + : "Document search failed"; } else if (props.actionType === "search") { return props.success ? "Search completed" : "Search failed"; } else if (props.actionType === "mcp") { @@ -227,6 +242,7 @@ function ActionItem(props: ActionItemProps) { props.actionType === "shell" || props.actionType === "install_dependencies" || props.actionType === "get_url_content" || + props.actionType === "search_document_for" || props.actionType === "search" ) { return !!props.output; @@ -284,6 +300,13 @@ function ActionItem(props: ActionItemProps) { icon={} /> ); + } else if (props.actionType === "search_document_for") { + return ( + } + /> + ); } else if (props.actionType === "mcp") { return ( ); + } else if (props.actionType === "search_document_for") { + return ( +
+ + {props.query} + +
+ {props.url} +
+
+ ); } else if (props.actionType === "mcp") { return (
@@ -440,6 +474,7 @@ function ActionItem(props: ActionItemProps) { if ( (props.actionType === "shell" || props.actionType === "search" || + props.actionType === "search_document_for" || props.actionType === "install_dependencies") && props.output ) { @@ -448,11 +483,14 @@ function ActionItem(props: ActionItemProps) {
             {props.output}
           
- {props.errorCode !== undefined && !props.success && ( -
- Exit code: {props.errorCode} -
- )} + {(props.actionType === "shell" || props.actionType === "search") && + "errorCode" in props && + props.errorCode !== undefined && + !props.success && ( +
+ Exit code: {props.errorCode} +
+ )}
); } else if (props.actionType === "mcp") { diff --git a/apps/web/src/components/thread/messages/ai.tsx b/apps/web/src/components/thread/messages/ai.tsx index 4e48aa1d..99132744 100644 --- a/apps/web/src/components/thread/messages/ai.tsx +++ b/apps/web/src/components/thread/messages/ai.tsx @@ -42,6 +42,7 @@ import { createCodeReviewMarkTaskNotCompleteFields, createDiagnoseErrorToolFields, createGetURLContentToolFields, + createSearchDocumentForToolFields, createWriteTechnicalNotesToolFields, createConversationHistorySummaryToolFields, createReviewStartedToolFields, @@ -92,6 +93,8 @@ type DiagnoseErrorToolArgs = z.infer; const getURLContentTool = createGetURLContentToolFields(); type GetURLContentToolArgs = z.infer; +const searchDocumentForTool = createSearchDocumentForToolFields(); +type SearchDocumentForToolArgs = z.infer; const writeTechnicalNotesTool = createWriteTechnicalNotesToolFields(); type WriteTechnicalNotesToolArgs = z.infer< @@ -261,6 +264,17 @@ export function mapToolMessageToActionStepProps( output: getContentString(message.content), reasoningText, }; + } else if (toolCall?.name === searchDocumentForTool.name) { + const args = toolCall.args as SearchDocumentForToolArgs; + return { + actionType: "search_document_for", + status, + success, + url: args.url || "", + query: args.query || "", + output: getContentString(message.content), + reasoningText, + }; } else if (toolCall && isMcpTool(toolCall.name)) { return { actionType: "mcp", @@ -637,6 +651,15 @@ export function AssistantMessage({ url: args?.url || "", output: "", } as ActionItemProps; + } else if (toolCall.name === searchDocumentForTool.name) { + const args = toolCall.args as SearchDocumentForToolArgs; + return { + actionType: "search_document_for", + status: "generating", + url: args?.url || "", + query: args?.query || "", + output: "", + } as ActionItemProps; } else { if (isMcpTool(toolCall.name)) { return { diff --git a/packages/shared/src/open-swe/planner/types.ts b/packages/shared/src/open-swe/planner/types.ts index 5111ab04..964bfb08 100644 --- a/packages/shared/src/open-swe/planner/types.ts +++ b/packages/shared/src/open-swe/planner/types.ts @@ -36,6 +36,16 @@ export const PlannerGraphStateObj = MessagesZodState.extend({ fn: (_state, update) => update, }, }), + /** + * Cache of fetched document content keyed by URLs. + */ + documentCache: withLangGraph(z.custom>(), { + reducer: { + schema: z.custom>(), + fn: (state, update) => ({ ...state, ...update }), + }, + default: () => ({}), + }), taskPlan: withLangGraph(z.custom(), { reducer: { schema: z.custom(), diff --git a/packages/shared/src/open-swe/tools.ts b/packages/shared/src/open-swe/tools.ts index 1e02de94..eae8e4c3 100644 --- a/packages/shared/src/open-swe/tools.ts +++ b/packages/shared/src/open-swe/tools.ts @@ -391,6 +391,29 @@ export function createGetURLContentToolFields() { }; } +export function createSearchDocumentForToolFields() { + const searchDocumentForSchema = z.object({ + url: z + .string() + .describe( + "The URL of the document to search within. This should be a URL that was previously fetched and processed.", + ), + query: z + .string() + .describe( + "The natural language query to search for within the document content. This query will be passed to an LLM which will use it to extract relevant content from the document. Be specific about what information you're looking for.", + ), + }); + + return { + name: "search_document_for", + description: + "Search for specific information within a previously fetched document using natural language queries. This tool is particularly useful when working with large documents that have been summarized with a table of contents. " + + "This tool should only be called after a documentation or a web page has been read and summarized as a table of contents and you need to search for specific information within the document.", + schema: searchDocumentForSchema, + }; +} + export function createWriteTechnicalNotesToolFields() { const writeTechnicalNotesSchema = z.object({ notes: z diff --git a/packages/shared/src/open-swe/types.ts b/packages/shared/src/open-swe/types.ts index 79c754b5..c45a0006 100644 --- a/packages/shared/src/open-swe/types.ts +++ b/packages/shared/src/open-swe/types.ts @@ -221,6 +221,16 @@ export const GraphAnnotation = MessagesZodState.extend({ fn: (_state, update) => update, }, }), + /** + * Cache of fetched document content keyed by URLs. + */ + documentCache: withLangGraph(z.custom>(), { + reducer: { + schema: z.custom>(), + fn: (state, update) => ({ ...state, ...update }), + }, + default: () => ({}), + }), /** * The ID of the Github issue this thread is associated with */