mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 07:23:15 +00:00
refactor: move target_repository from configuration to state (#44)
* Apply patch * Apply patch * Apply patch * update frontend to reflect state change * update scripts * cr --------- Co-authored-by: Harrison Chase <11986836+hwchase17@users.noreply.github.com> Co-authored-by: bracesproul <braceasproul@gmail.com>
This commit is contained in:
parent
efba99a4f4
commit
ff23ea49a8
12 changed files with 68 additions and 106 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { Client } from "@langchain/langgraph-sdk";
|
import { Client } from "@langchain/langgraph-sdk";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { GraphConfig } from "../src/types.js";
|
|
||||||
import { HumanResponse } from "@langchain/langgraph/prebuilt";
|
import { HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||||
import { createLogger, LogLevel } from "../src/utils/logger.js";
|
import { createLogger, LogLevel } from "../src/utils/logger.js";
|
||||||
|
|
||||||
|
|
@ -25,22 +24,17 @@ I want the server to be able to authenticate users with GitHub, such that we wil
|
||||||
3. make pull requests and push changes to the repositories they give us access to
|
3. make pull requests and push changes to the repositories they give us access to
|
||||||
Once you're done, ensure you've documented the development process in the readme of this new app.`;
|
Once you're done, ensure you've documented the development process in the readme of this new app.`;
|
||||||
|
|
||||||
const configurable: Omit<
|
const targetRepository = {
|
||||||
GraphConfig["configurable"],
|
owner: "langchain-ai",
|
||||||
"thread_id" | "assistant_id"
|
repo: "open-swe",
|
||||||
> = {
|
|
||||||
target_repository: {
|
|
||||||
owner: "langchain-ai",
|
|
||||||
repo: "open-swe",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = client.runs.stream(threadId, "open-swe", {
|
const stream = client.runs.stream(threadId, "open-swe", {
|
||||||
input: {
|
input: {
|
||||||
messages: [{ role: "user", content: userRequest }],
|
messages: [{ role: "user", content: userRequest }],
|
||||||
|
targetRepository,
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
configurable,
|
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
},
|
},
|
||||||
ifNotExists: "create",
|
ifNotExists: "create",
|
||||||
|
|
@ -69,22 +63,12 @@ async function resumeGraph(threadId: string) {
|
||||||
args: null,
|
args: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const configurable: Omit<
|
|
||||||
GraphConfig["configurable"],
|
|
||||||
"thread_id" | "assistant_id"
|
|
||||||
> = {
|
|
||||||
target_repository: {
|
|
||||||
owner: "langchain-ai",
|
|
||||||
repo: "open-swe",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const stream = client.runs.stream(threadId, "open-swe", {
|
const stream = client.runs.stream(threadId, "open-swe", {
|
||||||
command: {
|
command: {
|
||||||
resume: resumeValue,
|
resume: resumeValue,
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
configurable,
|
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
},
|
},
|
||||||
streamSubgraphs: true,
|
streamSubgraphs: true,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { Client } from "@langchain/langgraph-sdk";
|
import { Client } from "@langchain/langgraph-sdk";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { GraphConfig } from "../src/types.js";
|
|
||||||
import { graph } from "../src/index.js";
|
import { graph } from "../src/index.js";
|
||||||
import { createLogger, LogLevel } from "../src/utils/logger.js";
|
import { createLogger, LogLevel } from "../src/utils/logger.js";
|
||||||
|
|
||||||
|
|
@ -15,7 +14,13 @@ async function runFromPlan() {
|
||||||
|
|
||||||
const threadId = uuidv4();
|
const threadId = uuidv4();
|
||||||
|
|
||||||
|
const targetRepository = {
|
||||||
|
owner: "langchain-ai",
|
||||||
|
repo: "open-swe",
|
||||||
|
};
|
||||||
|
|
||||||
const inputs = {
|
const inputs = {
|
||||||
|
targetRepository,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
|
|
@ -111,21 +116,9 @@ The user wants to implement a GitHub OAuth authentication server in the \`/apps/
|
||||||
branchName: `open-swe/${threadId}`,
|
branchName: `open-swe/${threadId}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const configurable: Omit<
|
|
||||||
GraphConfig["configurable"],
|
|
||||||
"thread_id" | "assistant_id"
|
|
||||||
> = {
|
|
||||||
target_repository: {
|
|
||||||
owner: "langchain-ai",
|
|
||||||
repo: "open-swe",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.info("Initializing sandbox...");
|
logger.info("Initializing sandbox...");
|
||||||
|
|
||||||
const initResult = await graph.nodes.initialize.invoke(inputs as any, {
|
const initResult = await graph.nodes.initialize.invoke(inputs as any);
|
||||||
configurable,
|
|
||||||
});
|
|
||||||
if (!initResult.sandboxSessionId) {
|
if (!initResult.sandboxSessionId) {
|
||||||
throw new Error("Failed to initialize sandbox.");
|
throw new Error("Failed to initialize sandbox.");
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +136,6 @@ The user wants to implement a GitHub OAuth authentication server in the \`/apps/
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
configurable,
|
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
},
|
},
|
||||||
ifNotExists: "create",
|
ifNotExists: "create",
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,8 @@ Once again, here are the completed tasks, remaining tasks, and the current task
|
||||||
{PLAN_PROMPT}
|
{PLAN_PROMPT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const formatPrompt = (state: GraphState, config: GraphConfig): string => {
|
const formatPrompt = (state: GraphState): string => {
|
||||||
const repoDirectory = getRepoAbsolutePath(config);
|
const repoDirectory = getRepoAbsolutePath(state.targetRepository);
|
||||||
return systemPrompt
|
return systemPrompt
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"{PLAN_PROMPT_WITH_SUMMARIES}",
|
"{PLAN_PROMPT_WITH_SUMMARIES}",
|
||||||
|
|
@ -128,7 +128,7 @@ export async function generateAction(
|
||||||
const response = await modelWithTools.invoke([
|
const response = await modelWithTools.invoke([
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: formatPrompt(state, config),
|
content: formatPrompt(state),
|
||||||
},
|
},
|
||||||
...state.messages,
|
...state.messages,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,10 @@ export async function initialize(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { target_repository } = config.configurable;
|
const { targetRepository } = state;
|
||||||
|
if (!targetRepository) {
|
||||||
if (!target_repository) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Missing required configuration. Please provide a git repository URL.",
|
"Missing required target repository. Please provide a git repository in state or configuration.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +96,7 @@ export async function initialize(
|
||||||
TIMEOUT_EXTENSION_OPT,
|
TIMEOUT_EXTENSION_OPT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await cloneRepo(sandbox, target_repository);
|
const res = await cloneRepo(sandbox, targetRepository);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
// TODO: This should probably be an interrupt.
|
// TODO: This should probably be an interrupt.
|
||||||
logger.error("Failed to clone repository", res.error);
|
logger.error("Failed to clone repository", res.error);
|
||||||
|
|
@ -105,7 +104,7 @@ export async function initialize(
|
||||||
}
|
}
|
||||||
logger.info("Repository cloned successfully.");
|
logger.info("Repository cloned successfully.");
|
||||||
|
|
||||||
const absoluteRepoDir = getRepoAbsolutePath(config);
|
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
|
||||||
|
|
||||||
logger.info(`Configuring git user for repository at "${absoluteRepoDir}"...`);
|
logger.info(`Configuring git user for repository at "${absoluteRepoDir}"...`);
|
||||||
await configureGitUserInRepo(absoluteRepoDir, sandbox);
|
await configureGitUserInRepo(absoluteRepoDir, sandbox);
|
||||||
|
|
@ -125,5 +124,6 @@ export async function initialize(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sandboxSessionId: sandbox.sandboxId,
|
sandboxSessionId: sandbox.sandboxId,
|
||||||
|
targetRepository,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export async function openPullRequest(
|
||||||
|
|
||||||
const sandbox = await Sandbox.connect(sandboxSessionId);
|
const sandbox = await Sandbox.connect(sandboxSessionId);
|
||||||
|
|
||||||
const { owner, repo } = config.configurable?.target_repository ?? {};
|
const { owner, repo } = state.targetRepository;
|
||||||
|
|
||||||
if (!owner || !repo) {
|
if (!owner || !repo) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -77,7 +77,7 @@ export async function openPullRequest(
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedFiles = await getChangedFilesStatus(
|
const changedFiles = await getChangedFilesStatus(
|
||||||
getRepoAbsolutePath(config),
|
getRepoAbsolutePath(state.targetRepository),
|
||||||
sandbox,
|
sandbox,
|
||||||
);
|
);
|
||||||
let branchName = state.branchName;
|
let branchName = state.branchName;
|
||||||
|
|
@ -85,9 +85,14 @@ export async function openPullRequest(
|
||||||
logger.info(`Has ${changedFiles.length} changed files. Committing.`, {
|
logger.info(`Has ${changedFiles.length} changed files. Committing.`, {
|
||||||
changedFiles,
|
changedFiles,
|
||||||
});
|
});
|
||||||
branchName = await checkoutBranchAndCommit(config, sandbox, {
|
branchName = await checkoutBranchAndCommit(
|
||||||
branchName,
|
config,
|
||||||
});
|
state.targetRepository,
|
||||||
|
sandbox,
|
||||||
|
{
|
||||||
|
branchName,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = await loadModel(config, Task.SUMMARIZER);
|
const model = await loadModel(config, Task.SUMMARIZER);
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ export async function takeAction(
|
||||||
// If there are, commit them.
|
// If there are, commit them.
|
||||||
const sandbox = await Sandbox.connect(state.sandboxSessionId);
|
const sandbox = await Sandbox.connect(state.sandboxSessionId);
|
||||||
const changedFiles = await getChangedFilesStatus(
|
const changedFiles = await getChangedFilesStatus(
|
||||||
getRepoAbsolutePath(config),
|
getRepoAbsolutePath(state.targetRepository),
|
||||||
sandbox,
|
sandbox,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -138,9 +138,14 @@ export async function takeAction(
|
||||||
logger.info(`Has ${changedFiles.length} changed files. Committing.`, {
|
logger.info(`Has ${changedFiles.length} changed files. Committing.`, {
|
||||||
changedFiles,
|
changedFiles,
|
||||||
});
|
});
|
||||||
branchName = await checkoutBranchAndCommit(config, sandbox, {
|
branchName = await checkoutBranchAndCommit(
|
||||||
branchName,
|
config,
|
||||||
});
|
state.targetRepository,
|
||||||
|
sandbox,
|
||||||
|
{
|
||||||
|
branchName,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldRouteDiagnoseNode = shouldDiagnoseError(
|
const shouldRouteDiagnoseNode = shouldDiagnoseError(
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,12 @@ export const GraphAnnotation = z.object({
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.langgraph.reducer((_state, update) => update),
|
.langgraph.reducer((_state, update) => update),
|
||||||
|
/**
|
||||||
|
* The target repository information
|
||||||
|
*/
|
||||||
|
targetRepository: z
|
||||||
|
.custom<TargetRepository>()
|
||||||
|
.langgraph.reducer((_state, update) => update),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GraphState = z.infer<typeof GraphAnnotation>;
|
export type GraphState = z.infer<typeof GraphAnnotation>;
|
||||||
|
|
@ -143,25 +149,6 @@ const MODEL_OPTIONS_NO_THINKING = MODEL_OPTIONS.filter(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GraphConfiguration = z.object({
|
export const GraphConfiguration = z.object({
|
||||||
/**
|
|
||||||
* The URL of the repository to clone.
|
|
||||||
*/
|
|
||||||
target_repository: z
|
|
||||||
.object({
|
|
||||||
owner: z.string(),
|
|
||||||
repo: z.string(),
|
|
||||||
branch: z.string().optional(),
|
|
||||||
})
|
|
||||||
.langgraph.metadata({
|
|
||||||
x_oap_ui_config: {
|
|
||||||
type: "json",
|
|
||||||
default: `{
|
|
||||||
"owner": "",
|
|
||||||
"repo": "",
|
|
||||||
"branch": ""
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
/**
|
/**
|
||||||
* The model ID to use for the planning step.
|
* The model ID to use for the planning step.
|
||||||
* This includes initial planning, and rewriting.
|
* This includes initial planning, and rewriting.
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { CommandResult, Sandbox } from "@e2b/code-interpreter";
|
import { CommandResult, Sandbox } from "@e2b/code-interpreter";
|
||||||
import { createLogger, LogLevel } from "../logger.js";
|
import { createLogger, LogLevel } from "../logger.js";
|
||||||
import { GraphConfig } from "../../types.js";
|
import { GraphConfig, TargetRepository } from "../../types.js";
|
||||||
import { TIMEOUT_MS } from "../../constants.js";
|
import { TIMEOUT_MS } from "../../constants.js";
|
||||||
import { getSandboxErrorFields } from "../sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "../sandbox-error-fields.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GitUtil");
|
const logger = createLogger(LogLevel.INFO, "GitUtil");
|
||||||
|
|
||||||
export function getRepoAbsolutePath(config: GraphConfig): string {
|
export function getRepoAbsolutePath(
|
||||||
const repoName = config.configurable?.target_repository.repo;
|
targetRepository: TargetRepository,
|
||||||
|
): string {
|
||||||
|
const repoName = targetRepository.repo;
|
||||||
if (!repoName) {
|
if (!repoName) {
|
||||||
throw new Error("No repository name provided");
|
throw new Error("No repository name provided");
|
||||||
}
|
}
|
||||||
|
|
@ -374,13 +376,14 @@ export async function getChangedFilesStatus(
|
||||||
|
|
||||||
export async function checkoutBranchAndCommit(
|
export async function checkoutBranchAndCommit(
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
|
targetRepository: TargetRepository,
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
options?: {
|
options?: {
|
||||||
branchName?: string;
|
branchName?: string;
|
||||||
},
|
},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
logger.info("Checking out branch and committing changes...");
|
logger.info("Checking out branch and committing changes...");
|
||||||
const absoluteRepoDir = getRepoAbsolutePath(config);
|
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
|
||||||
const branchName = options?.branchName || getBranchName(config);
|
const branchName = options?.branchName || getBranchName(config);
|
||||||
|
|
||||||
await checkoutBranch(absoluteRepoDir, branchName, sandbox);
|
await checkoutBranch(absoluteRepoDir, branchName, sandbox);
|
||||||
|
|
|
||||||
|
|
@ -89,13 +89,6 @@ export default function useInterruptedActions({
|
||||||
resume: response,
|
resume: response,
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
// TODO: Make configurable (just that target_repository object) & recursion limit in the UI.
|
|
||||||
configurable: {
|
|
||||||
target_repository: {
|
|
||||||
owner: "langchain-ai",
|
|
||||||
repo: "open-swe",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -215,8 +215,16 @@ export function Thread() {
|
||||||
const context =
|
const context =
|
||||||
Object.keys(artifactContext).length > 0 ? artifactContext : undefined;
|
Object.keys(artifactContext).length > 0 ? artifactContext : undefined;
|
||||||
|
|
||||||
|
const targetRepository = {
|
||||||
|
owner: "langchain-ai",
|
||||||
|
repo: "open-swe",
|
||||||
|
};
|
||||||
stream.submit(
|
stream.submit(
|
||||||
{ messages: [...toolMessages, newHumanMessage], context },
|
{
|
||||||
|
messages: [...toolMessages, newHumanMessage],
|
||||||
|
context,
|
||||||
|
targetRepository,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
streamMode: ["values"],
|
streamMode: ["values"],
|
||||||
optimisticValues: (prev) => ({
|
optimisticValues: (prev) => ({
|
||||||
|
|
@ -229,13 +237,6 @@ export function Thread() {
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
config: {
|
config: {
|
||||||
// TODO: Make configurable (just that target_repository object) & recursion limit in the UI.
|
|
||||||
configurable: {
|
|
||||||
target_repository: {
|
|
||||||
owner: "langchain-ai",
|
|
||||||
repo: "open-swe",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -255,13 +256,6 @@ export function Thread() {
|
||||||
checkpoint: parentCheckpoint,
|
checkpoint: parentCheckpoint,
|
||||||
streamMode: ["values"],
|
streamMode: ["values"],
|
||||||
config: {
|
config: {
|
||||||
// TODO: Make configurable (just that target_repository object) & recursion limit in the UI.
|
|
||||||
configurable: {
|
|
||||||
target_repository: {
|
|
||||||
owner: "langchain-ai",
|
|
||||||
repo: "open-swe",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -68,13 +68,6 @@ export function HumanMessage({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
// TODO: Make configurable (just that target_repository object) & recursion limit in the UI.
|
|
||||||
configurable: {
|
|
||||||
target_repository: {
|
|
||||||
owner: "langchain-ai",
|
|
||||||
repo: "open-swe",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,12 @@ import { TooltipIconButton } from "@/components/thread/tooltip-icon-button";
|
||||||
import { Copy, CopyCheck } from "lucide-react";
|
import { Copy, CopyCheck } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export type StateType = { messages: Message[]; ui?: UIMessage[] };
|
type TargetRepository = { owner: string; repo: string };
|
||||||
|
export type StateType = {
|
||||||
|
messages: Message[];
|
||||||
|
ui?: UIMessage[];
|
||||||
|
targetRepository?: TargetRepository;
|
||||||
|
};
|
||||||
|
|
||||||
const useTypedStream = useStream<
|
const useTypedStream = useStream<
|
||||||
StateType,
|
StateType,
|
||||||
|
|
@ -24,6 +29,7 @@ const useTypedStream = useStream<
|
||||||
messages?: Message[] | Message | string;
|
messages?: Message[] | Message | string;
|
||||||
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
|
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
|
||||||
context?: Record<string, unknown>;
|
context?: Record<string, unknown>;
|
||||||
|
targetRepository?: TargetRepository;
|
||||||
};
|
};
|
||||||
CustomEventType: UIMessage | RemoveUIMessage;
|
CustomEventType: UIMessage | RemoveUIMessage;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue