mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 01:33:15 +00:00
feat: Add install dependencies tool (#291)
* feat: Add install dependencies tool * cr * hookup prompt * frontend * add prompt saying deps installed
This commit is contained in:
parent
5ab7b94be9
commit
08a0945a0e
13 changed files with 318 additions and 29 deletions
12
apps/open-swe/src/constants.ts
Normal file
12
apps/open-swe/src/constants.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { SNAPSHOT_NAME } from "@open-swe/shared/constants";
|
||||||
|
import { CreateSandboxParams } from "@daytonaio/sdk";
|
||||||
|
|
||||||
|
export const DEFAULT_SANDBOX_CREATE_PARAMS: CreateSandboxParams = {
|
||||||
|
resources: {
|
||||||
|
cpu: 2,
|
||||||
|
memory: 4,
|
||||||
|
disk: 5,
|
||||||
|
},
|
||||||
|
user: "daytona",
|
||||||
|
image: SNAPSHOT_NAME,
|
||||||
|
};
|
||||||
|
|
@ -16,11 +16,16 @@ import { createLogger, LogLevel } from "../../../../utils/logger.js";
|
||||||
import { getCurrentPlanItem } from "../../../../utils/current-task.js";
|
import { getCurrentPlanItem } from "../../../../utils/current-task.js";
|
||||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||||
import { SYSTEM_PROMPT } from "./prompt.js";
|
import {
|
||||||
|
DEPENDENCIES_INSTALLED_PROMPT,
|
||||||
|
INSTALL_DEPENDENCIES_TOOL_PROMPT,
|
||||||
|
SYSTEM_PROMPT,
|
||||||
|
} from "./prompt.js";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
import { getMissingMessages } from "../../../../utils/github/issue-messages.js";
|
import { getMissingMessages } from "../../../../utils/github/issue-messages.js";
|
||||||
import { getTaskPlanFromIssue } from "../../../../utils/github/issue-task.js";
|
import { getTaskPlanFromIssue } from "../../../../utils/github/issue-task.js";
|
||||||
import { createRgTool } from "../../../../tools/rg.js";
|
import { createRgTool } from "../../../../tools/rg.js";
|
||||||
|
import { createInstallDependenciesTool } from "../../../../tools/install-dependencies.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
||||||
|
|
||||||
|
|
@ -50,7 +55,13 @@ const formatPrompt = (state: GraphState): string => {
|
||||||
state.codebaseTree || "No codebase tree generated yet.",
|
state.codebaseTree || "No codebase tree generated yet.",
|
||||||
)
|
)
|
||||||
.replaceAll("{CURRENT_WORKING_DIRECTORY}", repoDirectory)
|
.replaceAll("{CURRENT_WORKING_DIRECTORY}", repoDirectory)
|
||||||
.replaceAll("{CURRENT_TASK_NUMBER}", currentPlanItem.index.toString());
|
.replaceAll("{CURRENT_TASK_NUMBER}", currentPlanItem.index.toString())
|
||||||
|
.replaceAll(
|
||||||
|
"{INSTALL_DEPENDENCIES_TOOL_PROMPT}",
|
||||||
|
!state.dependenciesInstalled
|
||||||
|
? INSTALL_DEPENDENCIES_TOOL_PROMPT
|
||||||
|
: DEPENDENCIES_INSTALLED_PROMPT,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateAction(
|
export async function generateAction(
|
||||||
|
|
@ -64,6 +75,10 @@ export async function generateAction(
|
||||||
createApplyPatchTool(state),
|
createApplyPatchTool(state),
|
||||||
createRequestHumanHelpToolFields(),
|
createRequestHumanHelpToolFields(),
|
||||||
createUpdatePlanToolFields(),
|
createUpdatePlanToolFields(),
|
||||||
|
// Only provide the dependencies installed tool if they're not already installed.
|
||||||
|
...(state.dependenciesInstalled
|
||||||
|
? []
|
||||||
|
: [createInstallDependenciesTool(state)]),
|
||||||
];
|
];
|
||||||
const modelWithTools = model.bindTools(tools, {
|
const modelWithTools = model.bindTools(tools, {
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
export const INSTALL_DEPENDENCIES_TOOL_PROMPT = `* Use \`install_dependencies\` to install dependencies (skip if installation fails). IMPORTANT: You should only call this tool if you're executing a task which REQUIRES installing dependencies. Keep in mind that not all tasks will require installing dependencies.`;
|
||||||
|
export const DEPENDENCIES_INSTALLED_PROMPT = `* Dependencies have already been installed. *`;
|
||||||
|
|
||||||
export const SYSTEM_PROMPT = `# Identity
|
export const SYSTEM_PROMPT = `# Identity
|
||||||
|
|
||||||
You are a terminal-based agentic coding assistant built by LangChain. You wrap LLM models to enable natural language interaction with local codebases. You are precise, safe, and helpful.
|
You are a terminal-based agentic coding assistant built by LangChain. You wrap LLM models to enable natural language interaction with local codebases. You are precise, safe, and helpful.
|
||||||
|
|
@ -33,6 +36,7 @@ You are currently executing a specific task from a pre-generated plan. You have
|
||||||
* Use \`apply_patch\` for file edits (accepts diffs and file paths)
|
* Use \`apply_patch\` for file edits (accepts diffs and file paths)
|
||||||
* Use \`shell\` with \`touch\` to create new files (not \`apply_patch\`)
|
* Use \`shell\` with \`touch\` to create new files (not \`apply_patch\`)
|
||||||
* Always use \`workdir\` parameter instead of \`cd\` when running commands via the \`shell\` tool
|
* Always use \`workdir\` parameter instead of \`cd\` when running commands via the \`shell\` tool
|
||||||
|
{INSTALL_DEPENDENCIES_TOOL_PROMPT}
|
||||||
|
|
||||||
### Tool Usage Best Practices
|
### Tool Usage Best Practices
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,10 @@ export async function openPullRequest(
|
||||||
return {
|
return {
|
||||||
messages: newMessages,
|
messages: newMessages,
|
||||||
internalMessages: newMessages,
|
internalMessages: newMessages,
|
||||||
// If the sandbox was successfully deleted, we can remove it from the state.
|
// If the sandbox was successfully deleted, we can remove it from the state & reset the dependencies installed flag.
|
||||||
...(sandboxDeleted && { sandboxSessionId: undefined }),
|
...(sandboxDeleted && {
|
||||||
|
sandboxSessionId: undefined,
|
||||||
|
dependenciesInstalled: false,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import { daytonaClient } from "../../../utils/sandbox.js";
|
||||||
import { getCodebaseTree } from "../../../utils/tree.js";
|
import { getCodebaseTree } from "../../../utils/tree.js";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
import { shouldDiagnoseError } from "../utils/tool-message-error.js";
|
import { shouldDiagnoseError } from "../utils/tool-message-error.js";
|
||||||
|
import { createInstallDependenciesTool } from "../../../tools/install-dependencies.js";
|
||||||
|
import { createRgTool } from "../../../tools/rg.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "TakeAction");
|
const logger = createLogger(LogLevel.INFO, "TakeAction");
|
||||||
|
|
||||||
|
|
@ -41,9 +43,13 @@ export async function takeAction(
|
||||||
|
|
||||||
const applyPatchTool = createApplyPatchTool(state);
|
const applyPatchTool = createApplyPatchTool(state);
|
||||||
const shellTool = createShellTool(state);
|
const shellTool = createShellTool(state);
|
||||||
|
const rgTool = createRgTool(state);
|
||||||
|
const installDependenciesTool = createInstallDependenciesTool(state);
|
||||||
const toolsMap = {
|
const toolsMap = {
|
||||||
[applyPatchTool.name]: applyPatchTool,
|
[applyPatchTool.name]: applyPatchTool,
|
||||||
[shellTool.name]: shellTool,
|
[shellTool.name]: shellTool,
|
||||||
|
[rgTool.name]: rgTool,
|
||||||
|
[installDependenciesTool.name]: installDependenciesTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolCalls = lastMessage.tool_calls;
|
const toolCalls = lastMessage.tool_calls;
|
||||||
|
|
@ -107,6 +113,13 @@ export async function takeAction(
|
||||||
|
|
||||||
const toolCallResults = await Promise.all(toolCallResultsPromise);
|
const toolCallResults = await Promise.all(toolCallResultsPromise);
|
||||||
|
|
||||||
|
let wereDependenciesInstalled: boolean | null = null;
|
||||||
|
toolCallResults.forEach((toolCallResult) => {
|
||||||
|
if (toolCallResult.name === installDependenciesTool.name) {
|
||||||
|
wereDependenciesInstalled = toolCallResult.status === "success";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Always check if there are changed files after running a tool.
|
// Always check if there are changed files after running a tool.
|
||||||
// If there are, commit them.
|
// If there are, commit them.
|
||||||
const sandbox = await daytonaClient().get(state.sandboxSessionId);
|
const sandbox = await daytonaClient().get(state.sandboxSessionId);
|
||||||
|
|
@ -142,6 +155,9 @@ export async function takeAction(
|
||||||
internalMessages: toolCallResults,
|
internalMessages: toolCallResults,
|
||||||
...(branchName && { branchName }),
|
...(branchName && { branchName }),
|
||||||
codebaseTree,
|
codebaseTree,
|
||||||
|
...(wereDependenciesInstalled !== null && {
|
||||||
|
dependenciesInstalled: wereDependenciesInstalled,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
return new Command({
|
return new Command({
|
||||||
goto: shouldRouteDiagnoseNode ? "diagnose-error" : "progress-plan-step",
|
goto: shouldRouteDiagnoseNode ? "diagnose-error" : "progress-plan-step",
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,14 @@ import {
|
||||||
pullLatestChanges,
|
pullLatestChanges,
|
||||||
} from "../../utils/github/git.js";
|
} from "../../utils/github/git.js";
|
||||||
import { getCodebaseTree } from "../../utils/tree.js";
|
import { getCodebaseTree } from "../../utils/tree.js";
|
||||||
import {
|
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
||||||
DO_NOT_RENDER_ID_PREFIX,
|
|
||||||
SNAPSHOT_NAME,
|
|
||||||
} from "@open-swe/shared/constants";
|
|
||||||
import {
|
import {
|
||||||
CustomNodeEvent,
|
CustomNodeEvent,
|
||||||
INITIALIZE_NODE_ID,
|
INITIALIZE_NODE_ID,
|
||||||
} from "@open-swe/shared/open-swe/custom-node-events";
|
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
import { Sandbox } from "@daytonaio/sdk";
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
import { AIMessage, BaseMessage } from "@langchain/core/messages";
|
import { AIMessage, BaseMessage } from "@langchain/core/messages";
|
||||||
|
import { DEFAULT_SANDBOX_CREATE_PARAMS } from "../../constants.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "InitializeSandbox");
|
const logger = createLogger(LogLevel.INFO, "InitializeSandbox");
|
||||||
|
|
||||||
|
|
@ -30,6 +28,7 @@ type InitializeSandboxState = {
|
||||||
sandboxSessionId?: string;
|
sandboxSessionId?: string;
|
||||||
codebaseTree?: string;
|
codebaseTree?: string;
|
||||||
messages?: BaseMessage[];
|
messages?: BaseMessage[];
|
||||||
|
dependenciesInstalled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function initializeSandbox(
|
export async function initializeSandbox(
|
||||||
|
|
@ -207,7 +206,7 @@ export async function initializeSandbox(
|
||||||
emitStepEvent(baseCreateSandboxAction, "pending");
|
emitStepEvent(baseCreateSandboxAction, "pending");
|
||||||
let sandbox: Sandbox;
|
let sandbox: Sandbox;
|
||||||
try {
|
try {
|
||||||
sandbox = await daytonaClient().create({ image: SNAPSHOT_NAME });
|
sandbox = await daytonaClient().create(DEFAULT_SANDBOX_CREATE_PARAMS);
|
||||||
emitStepEvent(baseCreateSandboxAction, "success");
|
emitStepEvent(baseCreateSandboxAction, "success");
|
||||||
} catch {
|
} catch {
|
||||||
emitStepEvent(
|
emitStepEvent(
|
||||||
|
|
@ -332,5 +331,6 @@ export async function initializeSandbox(
|
||||||
targetRepository,
|
targetRepository,
|
||||||
codebaseTree,
|
codebaseTree,
|
||||||
messages: createEventsMessage(),
|
messages: createEventsMessage(),
|
||||||
|
dependenciesInstalled: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { test, expect } from "@jest/globals";
|
import { test, expect } from "@jest/globals";
|
||||||
import { daytonaClient } from "../utils/sandbox.js";
|
import { daytonaClient } from "../utils/sandbox.js";
|
||||||
import { SNAPSHOT_NAME } from "@open-swe/shared/constants";
|
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
|
||||||
|
import { DEFAULT_SANDBOX_CREATE_PARAMS } from "../constants.js";
|
||||||
|
|
||||||
test("Can execute rg commands", async () => {
|
test.skip("Can execute rg commands", async () => {
|
||||||
const githubToken = process.env.GITHUB_PAT;
|
const githubToken = process.env.GITHUB_PAT;
|
||||||
if (!githubToken) {
|
if (!githubToken) {
|
||||||
throw new Error("GITHUB_PAT environment variable is not set");
|
throw new Error("GITHUB_PAT environment variable is not set");
|
||||||
|
|
@ -12,10 +13,7 @@ test("Can execute rg commands", async () => {
|
||||||
const client = daytonaClient();
|
const client = daytonaClient();
|
||||||
|
|
||||||
console.log("Setting up sandbox...");
|
console.log("Setting up sandbox...");
|
||||||
const sandbox = await client.create({
|
const sandbox = await client.create(DEFAULT_SANDBOX_CREATE_PARAMS);
|
||||||
image: SNAPSHOT_NAME,
|
|
||||||
user: "daytona",
|
|
||||||
});
|
|
||||||
console.log("Setup sandbox:", sandbox.id);
|
console.log("Setup sandbox:", sandbox.id);
|
||||||
|
|
||||||
const repoUrlWithToken = `https://x-access-token:${githubToken}@github.com/langchain-ai/open-swe.git`;
|
const repoUrlWithToken = `https://x-access-token:${githubToken}@github.com/langchain-ai/open-swe.git`;
|
||||||
|
|
@ -41,3 +39,51 @@ OPEN_SWE_X
|
||||||
|
|
||||||
expect(testRes.exitCode).toBe(0);
|
expect(testRes.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DEFAULT_ENV = {
|
||||||
|
// Prevents corepack from showing a y/n download prompt which causes the command to hang
|
||||||
|
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
test("Installing dependencies", async () => {
|
||||||
|
const githubToken = process.env.GITHUB_PAT;
|
||||||
|
if (!githubToken) {
|
||||||
|
throw new Error("GITHUB_PAT environment variable is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = daytonaClient();
|
||||||
|
|
||||||
|
console.log("Setting up sandbox...");
|
||||||
|
const sandbox = await client.create(DEFAULT_SANDBOX_CREATE_PARAMS);
|
||||||
|
console.log("Setup sandbox:", sandbox.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const repoUrlWithToken = `https://x-access-token:${githubToken}@github.com/langchain-ai/open-swe.git`;
|
||||||
|
const cloneCommand = `git clone ${repoUrlWithToken}`;
|
||||||
|
|
||||||
|
console.log("Cloning repo...");
|
||||||
|
const cloneRes = await sandbox.process.executeCommand(
|
||||||
|
cloneCommand,
|
||||||
|
SANDBOX_ROOT_DIR,
|
||||||
|
);
|
||||||
|
expect(cloneRes.exitCode).toBe(0);
|
||||||
|
|
||||||
|
const installCommand = "yarn install";
|
||||||
|
const installRes = await sandbox.process.executeCommand(
|
||||||
|
installCommand,
|
||||||
|
`${SANDBOX_ROOT_DIR}/open-swe`,
|
||||||
|
DEFAULT_ENV,
|
||||||
|
120, // 120 seconds timeout
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`install res status: ${installRes.exitCode}\ninstall res output: ${installRes.result}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Install res exit code: ${installRes.exitCode}\nInstall res output: ${installRes.result}`,
|
||||||
|
);
|
||||||
|
expect(installRes.exitCode).toBe(0);
|
||||||
|
} finally {
|
||||||
|
await sandbox.delete();
|
||||||
|
console.log("Deleted sandbox:", sandbox.id);
|
||||||
|
}
|
||||||
|
}, 300_000); // 5 minutes timeout
|
||||||
|
|
|
||||||
101
apps/open-swe/src/tools/install-dependencies.ts
Normal file
101
apps/open-swe/src/tools/install-dependencies.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { tool } from "@langchain/core/tools";
|
||||||
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
|
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { getCurrentTaskInput } from "@langchain/langgraph";
|
||||||
|
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
|
||||||
|
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||||
|
import { daytonaClient } from "../utils/sandbox.js";
|
||||||
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
|
import { createInstallDependenciesToolFields } from "@open-swe/shared/open-swe/tools";
|
||||||
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.INFO, "InstallDependenciesTool");
|
||||||
|
|
||||||
|
const DEFAULT_ENV = {
|
||||||
|
// Prevents corepack from showing a y/n download prompt which causes the command to hang
|
||||||
|
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createInstallDependenciesTool(
|
||||||
|
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
||||||
|
) {
|
||||||
|
const installDependenciesTool = tool(
|
||||||
|
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
||||||
|
let sandbox: Sandbox | undefined;
|
||||||
|
try {
|
||||||
|
const state = getCurrentTaskInput<GraphState>();
|
||||||
|
const { sandboxSessionId } = state;
|
||||||
|
if (!sandboxSessionId) {
|
||||||
|
logger.error(
|
||||||
|
"FAILED TO INSTALL DEPENDENCIES: No sandbox session ID provided",
|
||||||
|
{
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"FAILED TO INSTALL DEPENDENCIES: No sandbox session ID provided",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoRoot = getRepoAbsolutePath(state.targetRepository);
|
||||||
|
|
||||||
|
sandbox = await daytonaClient().get(sandboxSessionId);
|
||||||
|
const command = input.command.join(" ");
|
||||||
|
const workdir = input.workdir || repoRoot;
|
||||||
|
logger.info("Running install dependencies command", {
|
||||||
|
command,
|
||||||
|
workdir,
|
||||||
|
});
|
||||||
|
const response = await sandbox.process.executeCommand(
|
||||||
|
command,
|
||||||
|
workdir,
|
||||||
|
DEFAULT_ENV,
|
||||||
|
TIMEOUT_SEC * 2.5, // add a 2.5 min timeout
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.exitCode !== 0) {
|
||||||
|
logger.error("Failed to install dependencies", {
|
||||||
|
error: response.result,
|
||||||
|
error_result: response,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
throw new Error(
|
||||||
|
`Command failed. Exit code: ${response.exitCode}\nResult: ${response.result}\nStdout:\n${response.artifacts?.stdout}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: response.result,
|
||||||
|
status: "success",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
const errorFields = getSandboxErrorFields(e);
|
||||||
|
if (errorFields) {
|
||||||
|
logger.error("Failed to install dependencies", {
|
||||||
|
input,
|
||||||
|
error: errorFields,
|
||||||
|
});
|
||||||
|
throw new Error(
|
||||||
|
`Command failed. Exit code: ${errorFields.exitCode}\nError: ${errorFields.result ?? errorFields.artifacts?.stdout}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"Failed to install dependencies: " +
|
||||||
|
(e instanceof Error ? e.message : "Unknown error"),
|
||||||
|
{
|
||||||
|
error: e,
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"FAILED TO INSTALL DEPENDENCIES: " +
|
||||||
|
(e instanceof Error ? e.message : "Unknown error"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createInstallDependenciesToolFields(state.targetRepository),
|
||||||
|
);
|
||||||
|
|
||||||
|
return installDependenciesTool;
|
||||||
|
}
|
||||||
|
|
@ -6,18 +6,17 @@ export function truncateOutput(
|
||||||
output: string,
|
output: string,
|
||||||
options?: {
|
options?: {
|
||||||
/**
|
/**
|
||||||
* @default 10000
|
* @default 2500
|
||||||
*/
|
*/
|
||||||
numStartCharacters?: number;
|
numStartCharacters?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default 10000
|
* @default 2500
|
||||||
*/
|
*/
|
||||||
numEndCharacters?: number;
|
numEndCharacters?: number;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { numStartCharacters = 10000, numEndCharacters = 10000 } =
|
const { numStartCharacters = 2500, numEndCharacters = 2500 } = options ?? {};
|
||||||
options ?? {};
|
|
||||||
|
|
||||||
if (numStartCharacters < 0 || numEndCharacters < 0) {
|
if (numStartCharacters < 0 || numEndCharacters < 0) {
|
||||||
throw new Error("numStartCharacters and numEndCharacters must be >= 0");
|
throw new Error("numStartCharacters and numEndCharacters must be >= 0");
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,13 @@ import {
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
FileText,
|
FileText,
|
||||||
|
CloudDownload,
|
||||||
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
createApplyPatchToolFields,
|
createApplyPatchToolFields,
|
||||||
createShellToolFields,
|
createShellToolFields,
|
||||||
|
createInstallDependenciesToolFields,
|
||||||
formatRgCommand,
|
formatRgCommand,
|
||||||
RipgrepCommand,
|
RipgrepCommand,
|
||||||
} from "@open-swe/shared/open-swe/tools";
|
} from "@open-swe/shared/open-swe/tools";
|
||||||
|
|
@ -26,6 +29,10 @@ const shellTool = createShellToolFields(dummyRepo);
|
||||||
type ShellToolArgs = z.infer<typeof shellTool.schema>;
|
type ShellToolArgs = z.infer<typeof shellTool.schema>;
|
||||||
const applyPatchTool = createApplyPatchToolFields(dummyRepo);
|
const applyPatchTool = createApplyPatchToolFields(dummyRepo);
|
||||||
type ApplyPatchToolArgs = z.infer<typeof applyPatchTool.schema>;
|
type ApplyPatchToolArgs = z.infer<typeof applyPatchTool.schema>;
|
||||||
|
const installDependenciesTool = createInstallDependenciesToolFields(dummyRepo);
|
||||||
|
type InstallDependenciesToolArgs = z.infer<
|
||||||
|
typeof installDependenciesTool.schema
|
||||||
|
>;
|
||||||
|
|
||||||
// Common props for all action types
|
// Common props for all action types
|
||||||
type BaseActionProps = {
|
type BaseActionProps = {
|
||||||
|
|
@ -59,11 +66,19 @@ type RgActionProps = BaseActionProps &
|
||||||
errorCode?: number;
|
errorCode?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type InstallDependenciesActionProps = BaseActionProps &
|
||||||
|
Partial<InstallDependenciesToolArgs> & {
|
||||||
|
actionType: "install_dependencies";
|
||||||
|
output?: string;
|
||||||
|
errorCode?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionItemProps =
|
export type ActionItemProps =
|
||||||
| (BaseActionProps & { status: "loading" })
|
| (BaseActionProps & { status: "loading" })
|
||||||
| ShellActionProps
|
| ShellActionProps
|
||||||
| PatchActionProps
|
| PatchActionProps
|
||||||
| RgActionProps;
|
| RgActionProps
|
||||||
|
| InstallDependenciesActionProps;
|
||||||
|
|
||||||
export type ActionStepProps = {
|
export type ActionStepProps = {
|
||||||
actions: ActionItemProps[];
|
actions: ActionItemProps[];
|
||||||
|
|
@ -109,6 +124,8 @@ function ActionItem(props: ActionItemProps) {
|
||||||
return props.success ? "Patch applied" : "Patch failed";
|
return props.success ? "Patch applied" : "Patch failed";
|
||||||
} else if (props.actionType === "rg") {
|
} else if (props.actionType === "rg") {
|
||||||
return props.success ? "Search completed" : "Search failed";
|
return props.success ? "Search completed" : "Search failed";
|
||||||
|
} else if (props.actionType === "install_dependencies") {
|
||||||
|
return props.success ? "Dependencies installed" : "Installation failed";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +136,11 @@ function ActionItem(props: ActionItemProps) {
|
||||||
const shouldShowToggle = () => {
|
const shouldShowToggle = () => {
|
||||||
if (props.status !== "done") return false;
|
if (props.status !== "done") return false;
|
||||||
|
|
||||||
if (props.actionType === "shell" || props.actionType === "rg") {
|
if (
|
||||||
|
props.actionType === "shell" ||
|
||||||
|
props.actionType === "rg" ||
|
||||||
|
props.actionType === "install_dependencies"
|
||||||
|
) {
|
||||||
return !!props.output;
|
return !!props.output;
|
||||||
} else if (props.actionType === "apply-patch") {
|
} else if (props.actionType === "apply-patch") {
|
||||||
return !!props.diff;
|
return !!props.diff;
|
||||||
|
|
@ -135,11 +156,15 @@ function ActionItem(props: ActionItemProps) {
|
||||||
return <Loader2 className="text-muted-foreground mr-2 size-3.5" />;
|
return <Loader2 className="text-muted-foreground mr-2 size-3.5" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.actionType === "shell" ? (
|
if (props.actionType === "install_dependencies") {
|
||||||
<Terminal className="text-muted-foreground mr-2 size-3.5" />
|
return <CloudDownload className="text-muted-foreground mr-2 size-3.5" />;
|
||||||
) : (
|
} else if (props.actionType === "apply-patch") {
|
||||||
<FileCode className="text-muted-foreground mr-2 size-3.5" />
|
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 {
|
||||||
|
return <Terminal className="text-muted-foreground mr-2 size-3.5" />;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render the header content based on action type
|
// Render the header content based on action type
|
||||||
|
|
@ -152,7 +177,10 @@ function ActionItem(props: ActionItemProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.actionType === "shell") {
|
if (
|
||||||
|
props.actionType === "shell" ||
|
||||||
|
props.actionType === "install_dependencies"
|
||||||
|
) {
|
||||||
let commandStr = "";
|
let commandStr = "";
|
||||||
if (props.command) {
|
if (props.command) {
|
||||||
if (Array.isArray(props.command)) {
|
if (Array.isArray(props.command)) {
|
||||||
|
|
@ -215,7 +243,9 @@ function ActionItem(props: ActionItemProps) {
|
||||||
if (!expanded) return null;
|
if (!expanded) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(props.actionType === "shell" || props.actionType === "rg") &&
|
(props.actionType === "shell" ||
|
||||||
|
props.actionType === "rg" ||
|
||||||
|
props.actionType === "install_dependencies") &&
|
||||||
props.output
|
props.output
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
createShellToolFields,
|
createShellToolFields,
|
||||||
createSetTaskStatusToolFields,
|
createSetTaskStatusToolFields,
|
||||||
createRgToolFields,
|
createRgToolFields,
|
||||||
|
createInstallDependenciesToolFields,
|
||||||
} from "@open-swe/shared/open-swe/tools";
|
} from "@open-swe/shared/open-swe/tools";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
|
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
|
||||||
|
|
@ -41,6 +42,10 @@ const setTaskStatusTool = createSetTaskStatusToolFields();
|
||||||
type SetTaskStatusToolArgs = z.infer<typeof setTaskStatusTool.schema>;
|
type SetTaskStatusToolArgs = z.infer<typeof setTaskStatusTool.schema>;
|
||||||
const rgTool = createRgToolFields(dummyRepo);
|
const rgTool = createRgToolFields(dummyRepo);
|
||||||
type RgToolArgs = z.infer<typeof rgTool.schema>;
|
type RgToolArgs = z.infer<typeof rgTool.schema>;
|
||||||
|
const installDependenciesTool = createInstallDependenciesToolFields(dummyRepo);
|
||||||
|
type InstallDependenciesToolArgs = z.infer<
|
||||||
|
typeof installDependenciesTool.schema
|
||||||
|
>;
|
||||||
|
|
||||||
function CustomComponent({
|
function CustomComponent({
|
||||||
message,
|
message,
|
||||||
|
|
@ -148,6 +153,17 @@ export function mapToolMessageToActionStepProps(
|
||||||
output: getContentString(message.content),
|
output: getContentString(message.content),
|
||||||
reasoningText,
|
reasoningText,
|
||||||
};
|
};
|
||||||
|
} else if (toolCall?.name === installDependenciesTool.name) {
|
||||||
|
const args = toolCall.args as InstallDependenciesToolArgs;
|
||||||
|
return {
|
||||||
|
actionType: "install_dependencies",
|
||||||
|
status,
|
||||||
|
success,
|
||||||
|
command: args.command || "",
|
||||||
|
workdir: args.workdir || "",
|
||||||
|
output: getContentString(message.content),
|
||||||
|
reasoningText,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: "loading",
|
status: "loading",
|
||||||
|
|
@ -210,7 +226,8 @@ export function AssistantMessage({
|
||||||
(tc) =>
|
(tc) =>
|
||||||
tc.name === shellTool.name ||
|
tc.name === shellTool.name ||
|
||||||
tc.name === applyPatchTool.name ||
|
tc.name === applyPatchTool.name ||
|
||||||
tc.name === rgTool.name,
|
tc.name === rgTool.name ||
|
||||||
|
tc.name === installDependenciesTool.name,
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|
@ -248,6 +265,8 @@ export function AssistantMessage({
|
||||||
|
|
||||||
const isShellTool = toolCall.name === shellTool.name;
|
const isShellTool = toolCall.name === shellTool.name;
|
||||||
const isRgTool = toolCall.name === rgTool.name;
|
const isRgTool = toolCall.name === rgTool.name;
|
||||||
|
const isInstallDependenciesTool =
|
||||||
|
toolCall.name === installDependenciesTool.name;
|
||||||
|
|
||||||
if (correspondingToolResult) {
|
if (correspondingToolResult) {
|
||||||
// If we have a tool result, map it to action props
|
// If we have a tool result, map it to action props
|
||||||
|
|
@ -261,6 +280,15 @@ export function AssistantMessage({
|
||||||
paths: args?.paths || [],
|
paths: args?.paths || [],
|
||||||
output: "",
|
output: "",
|
||||||
} as ActionItemProps;
|
} as ActionItemProps;
|
||||||
|
} else if (isInstallDependenciesTool) {
|
||||||
|
const args = toolCall.args as InstallDependenciesToolArgs;
|
||||||
|
return {
|
||||||
|
actionType: "install_dependencies",
|
||||||
|
status: "generating",
|
||||||
|
command: args?.command || "",
|
||||||
|
workdir: args?.workdir || "",
|
||||||
|
output: "",
|
||||||
|
} as ActionItemProps;
|
||||||
} else {
|
} else {
|
||||||
if (isShellTool) {
|
if (isShellTool) {
|
||||||
const args = toolCall.args as ShellToolArgs;
|
const args = toolCall.args as ShellToolArgs;
|
||||||
|
|
|
||||||
|
|
@ -179,3 +179,28 @@ export function createSetTaskStatusToolFields() {
|
||||||
|
|
||||||
return setTaskStatusTool;
|
return setTaskStatusTool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createInstallDependenciesToolFields(
|
||||||
|
targetRepository: TargetRepository,
|
||||||
|
) {
|
||||||
|
const repoRoot = getRepoAbsolutePath(targetRepository);
|
||||||
|
|
||||||
|
const installDependenciesToolSchema = z.object({
|
||||||
|
command: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe("The command to run to install dependencies."),
|
||||||
|
workdir: z
|
||||||
|
.string()
|
||||||
|
.default(repoRoot)
|
||||||
|
.describe(
|
||||||
|
`The working directory to run the command in. The default working directory this command will be executed in is the root of the repository: \`${repoRoot}\`. If you want to execute this install command inside a different location, pass a path to this field.`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "install_dependencies",
|
||||||
|
description:
|
||||||
|
"Installs dependencies for the repository. You should only call this tool if you need to install dependencies for a specific task. Ensure you only call this tool after gathering context on how to install dependencies, such as the package manager, proper install command, etc.",
|
||||||
|
schema: installDependenciesToolSchema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,16 @@ export const GraphAnnotation = MessagesZodState.extend({
|
||||||
fn: (_state, update) => update,
|
fn: (_state, update) => update,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
/**
|
||||||
|
* Whether or not the dependencies have been installed already in the sandbox.
|
||||||
|
*/
|
||||||
|
dependenciesInstalled: withLangGraph(z.custom<boolean>(), {
|
||||||
|
reducer: {
|
||||||
|
schema: z.custom<boolean>(),
|
||||||
|
fn: (_state, update) => update,
|
||||||
|
},
|
||||||
|
default: () => false,
|
||||||
|
}),
|
||||||
|
|
||||||
// ---NOT USED---
|
// ---NOT USED---
|
||||||
ui: z
|
ui: z
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue