mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
feat: Add tool to get contents of URL (#340)
This commit is contained in:
parent
6f7c5e1111
commit
9082b86799
13 changed files with 1015 additions and 13 deletions
|
|
@ -24,11 +24,13 @@
|
|||
"dependencies": {
|
||||
"@daytonaio/sdk": "^0.18.1",
|
||||
"@langchain/anthropic": "^0.3.20",
|
||||
"@langchain/community": "^0.3.47",
|
||||
"@langchain/core": "^0.3.56",
|
||||
"@langchain/google-genai": "^0.2.9",
|
||||
"@langchain/langgraph": "^0.3.3",
|
||||
"@langchain/langgraph-sdk": "^0.0.85",
|
||||
"@langchain/openai": "^0.5.10",
|
||||
"@mendable/firecrawl-js": "^1.29.1",
|
||||
"@octokit/app": "^16.0.1",
|
||||
"@octokit/core": "^7.0.2",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { loadModel, Task } from "../../../../utils/load-model.js";
|
||||
import { createShellTool } from "../../../../tools/index.js";
|
||||
import {
|
||||
createGetURLContentTool,
|
||||
createShellTool,
|
||||
} from "../../../../tools/index.js";
|
||||
import {
|
||||
PlannerGraphState,
|
||||
PlannerGraphUpdate,
|
||||
|
|
@ -51,6 +54,7 @@ export async function generateAction(
|
|||
createRgTool(state),
|
||||
createShellTool(state),
|
||||
createPlannerNotesTool(),
|
||||
createGetURLContentTool(),
|
||||
];
|
||||
const modelWithTools = model.bindTools(tools, {
|
||||
tool_choice: "auto",
|
||||
|
|
|
|||
|
|
@ -19,9 +19,12 @@ Your sole objective in this phase is to gather comprehensive context about the c
|
|||
- You do not want to have to generate tasks such as 'Locate the XYZ file', 'Examine the structure of the codebase', or 'Do X if Y is true, otherwise to Z'.
|
||||
- To ensure the above does not happen, you should be thorough in your context gathering. Always gather enough context to cover all edge cases, and prevent unclear instructions.
|
||||
|
||||
4. **Leverage efficient search tools**: Use \`rg\` (ripgrep) for all file searches because it respects .gitignore patterns and provides significantly faster results than alternatives like grep or ls -R.
|
||||
- When searching for specific file types, use glob patterns: \`rg -i pattern -g **/*.tsx project-directory/\`
|
||||
- This explicit pattern matching ensures accurate results across all file extensions
|
||||
4. **Leverage efficient search tools**:
|
||||
- Use \`rg\` (ripgrep) for all file searches because it respects .gitignore patterns and provides significantly faster results than alternatives like grep or ls -R.
|
||||
- When searching for specific file types, use glob patterns: \`rg -i pattern -g **/*.tsx project-directory/\`
|
||||
- This explicit pattern matching ensures accurate results across all file extensions
|
||||
- If the user passes a URL, you should use the \`get_url_content\` tool to fetch the contents of the URL.
|
||||
- You should only use this tool to fetch the contents of a URL the user has provided, or that you've discovered during your context searching, which you believe is vital to gathering context for the user's request.
|
||||
|
||||
5. **Format shell commands precisely**: Ensure all shell commands include proper quoting and escaping. Well-formatted commands prevent errors and provide reliable results.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
|
||||
import { createShellTool } from "../../../tools/index.js";
|
||||
import {
|
||||
createGetURLContentTool,
|
||||
createShellTool,
|
||||
} from "../../../tools/index.js";
|
||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||
import {
|
||||
PlannerGraphState,
|
||||
|
|
@ -34,10 +37,12 @@ export async function takeActions(
|
|||
const shellTool = createShellTool(state);
|
||||
const rgTool = createRgTool(state);
|
||||
const plannerNotesTool = createPlannerNotesTool();
|
||||
const getURLContentTool = createGetURLContentTool();
|
||||
const toolsMap = {
|
||||
[shellTool.name]: shellTool,
|
||||
[rgTool.name]: rgTool,
|
||||
[plannerNotesTool.name]: plannerNotesTool,
|
||||
[getURLContentTool.name]: getURLContentTool,
|
||||
};
|
||||
|
||||
const toolCalls = lastMessage.tool_calls;
|
||||
|
|
@ -96,9 +101,18 @@ 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 toolMessage = new ToolMessage({
|
||||
tool_call_id: toolCall.id ?? "",
|
||||
content: truncateOutput(result),
|
||||
content: truncatedOutput,
|
||||
name: toolCall.name,
|
||||
status: toolCallStatus,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
createApplyPatchTool,
|
||||
createRequestHumanHelpToolFields,
|
||||
createUpdatePlanToolFields,
|
||||
createGetURLContentTool,
|
||||
} from "../../../../tools/index.js";
|
||||
import { formatPlanPrompt } from "../../../../utils/plan-prompt.js";
|
||||
import { stopSandbox } from "../../../../utils/sandbox.js";
|
||||
|
|
@ -77,6 +78,7 @@ export async function generateAction(
|
|||
createApplyPatchTool(state),
|
||||
createRequestHumanHelpToolFields(),
|
||||
createUpdatePlanToolFields(),
|
||||
createGetURLContentTool(),
|
||||
// Only provide the dependencies installed tool if they're not already installed.
|
||||
...(state.dependenciesInstalled
|
||||
? []
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ You are currently executing a specific task from a pre-generated plan. You have
|
|||
* **Pre-commit**: Run \`pre-commit run --files ...\` if .pre-commit-config.yaml exists
|
||||
* **History**: Use \`git log\` and \`git blame\` for additional context when needed
|
||||
* **Parallel Tool Calling**: You're allowed, and encouraged to call multiple tools at once, as long as they do not conflict, or depend on each other.
|
||||
* **URL Content**: Use the \`get_url_content\` tool to fetch the contents of a URL. You should only use this tool to fetch the contents of a URL the user has provided, or that you've discovered during your context searching, which you believe is vital to gathering context for the user's request.
|
||||
|
||||
### Coding Standards
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
|
||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||
import { createApplyPatchTool, createShellTool } from "../../../tools/index.js";
|
||||
import {
|
||||
createApplyPatchTool,
|
||||
createGetURLContentTool,
|
||||
createShellTool,
|
||||
} from "../../../tools/index.js";
|
||||
import {
|
||||
GraphState,
|
||||
GraphConfig,
|
||||
|
|
@ -45,11 +49,13 @@ export async function takeAction(
|
|||
const shellTool = createShellTool(state);
|
||||
const rgTool = createRgTool(state);
|
||||
const installDependenciesTool = createInstallDependenciesTool(state);
|
||||
const getURLContentTool = createGetURLContentTool();
|
||||
const toolsMap = {
|
||||
[applyPatchTool.name]: applyPatchTool,
|
||||
[shellTool.name]: shellTool,
|
||||
[rgTool.name]: rgTool,
|
||||
[installDependenciesTool.name]: installDependenciesTool,
|
||||
[getURLContentTool.name]: getURLContentTool,
|
||||
};
|
||||
|
||||
const toolCalls = lastMessage.tool_calls;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * from "./apply-patch.js";
|
||||
export * from "./shell.js";
|
||||
export * from "./url-content.js";
|
||||
export {
|
||||
createUpdatePlanToolFields,
|
||||
createSessionPlanToolFields,
|
||||
|
|
|
|||
52
apps/open-swe/src/tools/url-content.ts
Normal file
52
apps/open-swe/src/tools/url-content.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
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";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "GetURLContentTool");
|
||||
|
||||
export function createGetURLContentTool() {
|
||||
const getURLContentTool = tool(
|
||||
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const loader = new FireCrawlLoader({
|
||||
url: parsedUrl.href,
|
||||
mode: "scrape",
|
||||
params: {
|
||||
formats: ["markdown"],
|
||||
},
|
||||
});
|
||||
|
||||
const docs = await loader.load();
|
||||
const text = docs.map((doc) => doc.pageContent).join("\n\n");
|
||||
|
||||
return {
|
||||
result: text,
|
||||
status: "success",
|
||||
};
|
||||
} catch (e) {
|
||||
const errorString = e instanceof Error ? e.message : String(e);
|
||||
logger.error("Failed to get URL content", { url, error: errorString });
|
||||
return {
|
||||
result: `Failed to get URL content: ${url}\nError:\n${errorString}`,
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
},
|
||||
createGetURLContentToolFields(),
|
||||
);
|
||||
return getURLContentTool;
|
||||
}
|
||||
|
|
@ -13,12 +13,14 @@ import {
|
|||
FileText,
|
||||
CloudDownload,
|
||||
Search,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
createApplyPatchToolFields,
|
||||
createShellToolFields,
|
||||
createInstallDependenciesToolFields,
|
||||
createTakePlannerNotesFields,
|
||||
createGetURLContentToolFields,
|
||||
formatRgCommand,
|
||||
RipgrepCommand,
|
||||
} from "@open-swe/shared/open-swe/tools";
|
||||
|
|
@ -36,6 +38,8 @@ type InstallDependenciesToolArgs = z.infer<
|
|||
>;
|
||||
const plannerNotesTool = createTakePlannerNotesFields();
|
||||
type PlannerNotesToolArgs = z.infer<typeof plannerNotesTool.schema>;
|
||||
const getURLContentTool = createGetURLContentToolFields();
|
||||
type GetURLContentToolArgs = z.infer<typeof getURLContentTool.schema>;
|
||||
|
||||
// Common props for all action types
|
||||
type BaseActionProps = {
|
||||
|
|
@ -81,13 +85,20 @@ type PlannerNotesActionProps = BaseActionProps &
|
|||
actionType: "planner_notes";
|
||||
};
|
||||
|
||||
type GetURLContentActionProps = BaseActionProps &
|
||||
Partial<GetURLContentToolArgs> & {
|
||||
actionType: "get_url_content";
|
||||
output?: string;
|
||||
};
|
||||
|
||||
export type ActionItemProps =
|
||||
| (BaseActionProps & { status: "loading" })
|
||||
| ShellActionProps
|
||||
| PatchActionProps
|
||||
| RgActionProps
|
||||
| InstallDependenciesActionProps
|
||||
| PlannerNotesActionProps;
|
||||
| PlannerNotesActionProps
|
||||
| GetURLContentActionProps;
|
||||
|
||||
export type ActionStepProps = {
|
||||
actions: ActionItemProps[];
|
||||
|
|
@ -101,6 +112,7 @@ const ACTION_GENERATING_TEXT_MAP = {
|
|||
["rg"]: "Searching...",
|
||||
[installDependenciesTool.name]: "Installing dependencies...",
|
||||
[plannerNotesTool.name]: "Saving notes...",
|
||||
[getURLContentTool.name]: "Fetching URL content...",
|
||||
};
|
||||
|
||||
function ActionItem(props: ActionItemProps) {
|
||||
|
|
@ -143,6 +155,10 @@ function ActionItem(props: ActionItemProps) {
|
|||
return props.success ? "Dependencies installed" : "Installation failed";
|
||||
} else if (props.actionType === "planner_notes") {
|
||||
return props.success ? "Notes saved" : "Failed to save notes";
|
||||
} else if (props.actionType === "get_url_content") {
|
||||
return props.success
|
||||
? "URL content fetched"
|
||||
: "Failed to fetch URL content";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +172,8 @@ function ActionItem(props: ActionItemProps) {
|
|||
if (
|
||||
props.actionType === "shell" ||
|
||||
props.actionType === "rg" ||
|
||||
props.actionType === "install_dependencies"
|
||||
props.actionType === "install_dependencies" ||
|
||||
props.actionType === "get_url_content"
|
||||
) {
|
||||
return !!props.output;
|
||||
} else if (props.actionType === "apply-patch") {
|
||||
|
|
@ -183,6 +200,8 @@ function ActionItem(props: ActionItemProps) {
|
|||
return <FileCode className="text-muted-foreground mr-2 size-3.5" />;
|
||||
} else if (props.actionType === "rg") {
|
||||
return <Search className="text-muted-foreground mr-2 size-3.5" />;
|
||||
} else if (props.actionType === "get_url_content") {
|
||||
return <Globe className="text-muted-foreground mr-2 size-3.5" />;
|
||||
} else {
|
||||
return <Terminal className="text-muted-foreground mr-2 size-3.5" />;
|
||||
}
|
||||
|
|
@ -208,6 +227,16 @@ function ActionItem(props: ActionItemProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (props.actionType === "get_url_content") {
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<code className="text-foreground/80 text-xs font-normal">
|
||||
{props.url}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
props.actionType === "shell" ||
|
||||
props.actionType === "install_dependencies"
|
||||
|
|
@ -291,6 +320,14 @@ function ActionItem(props: ActionItemProps) {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (props.actionType === "get_url_content" && props.output) {
|
||||
return (
|
||||
<div className="bg-muted text-foreground/90 overflow-x-auto p-2 dark:bg-gray-900">
|
||||
<pre className="text-xs font-normal whitespace-pre-wrap">
|
||||
{props.output}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
} else if (props.actionType === "apply-patch" && props.diff) {
|
||||
return (
|
||||
<div className="bg-muted overflow-x-auto p-2 dark:bg-gray-900">
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
createInstallDependenciesToolFields,
|
||||
createTakePlannerNotesFields,
|
||||
createDiagnoseErrorToolFields,
|
||||
createGetURLContentToolFields,
|
||||
} from "@open-swe/shared/open-swe/tools";
|
||||
import { z } from "zod";
|
||||
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
|
||||
|
|
@ -59,6 +60,9 @@ type PlannerNotesToolArgs = z.infer<typeof plannerNotesTool.schema>;
|
|||
const diagnoseErrorTool = createDiagnoseErrorToolFields();
|
||||
type DiagnoseErrorToolArgs = z.infer<typeof diagnoseErrorTool.schema>;
|
||||
|
||||
const getURLContentTool = createGetURLContentToolFields();
|
||||
type GetURLContentToolArgs = z.infer<typeof getURLContentTool.schema>;
|
||||
|
||||
function CustomComponent({
|
||||
message,
|
||||
thread,
|
||||
|
|
@ -185,6 +189,16 @@ export function mapToolMessageToActionStepProps(
|
|||
notes: args.notes || [],
|
||||
reasoningText,
|
||||
};
|
||||
} else if (toolCall?.name === getURLContentTool.name) {
|
||||
const args = toolCall.args as GetURLContentToolArgs;
|
||||
return {
|
||||
actionType: "get_url_content",
|
||||
status,
|
||||
success,
|
||||
url: args.url || "",
|
||||
output: getContentString(message.content),
|
||||
reasoningText,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "loading",
|
||||
|
|
@ -249,7 +263,8 @@ export function AssistantMessage({
|
|||
tc.name === applyPatchTool.name ||
|
||||
tc.name === rgTool.name ||
|
||||
tc.name === installDependenciesTool.name ||
|
||||
tc.name === plannerNotesTool.name,
|
||||
tc.name === plannerNotesTool.name ||
|
||||
tc.name === getURLContentTool.name,
|
||||
)
|
||||
: [];
|
||||
|
||||
|
|
@ -397,6 +412,14 @@ export function AssistantMessage({
|
|||
status: "generating",
|
||||
notes: args?.notes || [],
|
||||
} as ActionItemProps;
|
||||
} else if (toolCall.name === getURLContentTool.name) {
|
||||
const args = toolCall.args as GetURLContentToolArgs;
|
||||
return {
|
||||
actionType: "get_url_content",
|
||||
status: "generating",
|
||||
url: args?.url || "",
|
||||
output: "",
|
||||
} as ActionItemProps;
|
||||
} else {
|
||||
if (isShellTool) {
|
||||
const args = toolCall.args as ShellToolArgs;
|
||||
|
|
|
|||
|
|
@ -281,3 +281,19 @@ export function createDiagnoseErrorToolFields() {
|
|||
schema: diagnoseErrorToolSchema,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetURLContentToolFields() {
|
||||
const getURLContentSchema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.describe(
|
||||
"The URL to get the content of. Returns the page content in markdown format.",
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
name: "get_url_content",
|
||||
description: "Get the full page content of a given URL in markdown format.",
|
||||
schema: getURLContentSchema,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue