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:
Harrison Chase 2025-05-29 20:07:28 -04:00 • committed by GitHub
parent efba99a4f4
commit ff23ea49a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 68 additions and 106 deletions

View file

@ -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,

View file

@ -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",

View file

@ -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,
]); ]);

View file

@ -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,
}; };
} }

View file

@ -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);

View file

@ -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(

View file

@ -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.

View file

@ -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);

View file

@ -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,
}, },
}, },

View file

@ -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,
}, },
}); });

View file

@ -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,
}, },
}, },

View file

@ -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;
} }