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 { Client } from "@langchain/langgraph-sdk";
import { v4 as uuidv4 } from "uuid";
import { GraphConfig } from "../src/types.js";
import { HumanResponse } from "@langchain/langgraph/prebuilt";
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
Once you're done, ensure you've documented the development process in the readme of this new app.`;
const configurable: Omit<
GraphConfig["configurable"],
"thread_id" | "assistant_id"
> = {
target_repository: {
owner: "langchain-ai",
repo: "open-swe",
},
const targetRepository = {
owner: "langchain-ai",
repo: "open-swe",
};
const stream = client.runs.stream(threadId, "open-swe", {
input: {
messages: [{ role: "user", content: userRequest }],
targetRepository,
},
config: {
configurable,
recursion_limit: 400,
},
ifNotExists: "create",
@ -69,22 +63,12 @@ async function resumeGraph(threadId: string) {
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", {
command: {
resume: resumeValue,
},
config: {
configurable,
recursion_limit: 400,
},
streamSubgraphs: true,

View file

@ -1,7 +1,6 @@
import "dotenv/config";
import { Client } from "@langchain/langgraph-sdk";
import { v4 as uuidv4 } from "uuid";
import { GraphConfig } from "../src/types.js";
import { graph } from "../src/index.js";
import { createLogger, LogLevel } from "../src/utils/logger.js";
@ -15,7 +14,13 @@ async function runFromPlan() {
const threadId = uuidv4();
const targetRepository = {
owner: "langchain-ai",
repo: "open-swe",
};
const inputs = {
targetRepository,
messages: [
{
role: "user",
@ -111,21 +116,9 @@ The user wants to implement a GitHub OAuth authentication server in the \`/apps/
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...");
const initResult = await graph.nodes.initialize.invoke(inputs as any, {
configurable,
});
const initResult = await graph.nodes.initialize.invoke(inputs as any);
if (!initResult.sandboxSessionId) {
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: {
configurable,
recursion_limit: 400,
},
ifNotExists: "create",

View file

@ -98,8 +98,8 @@ Once again, here are the completed tasks, remaining tasks, and the current task
{PLAN_PROMPT}
`;
const formatPrompt = (state: GraphState, config: GraphConfig): string => {
const repoDirectory = getRepoAbsolutePath(config);
const formatPrompt = (state: GraphState): string => {
const repoDirectory = getRepoAbsolutePath(state.targetRepository);
return systemPrompt
.replaceAll(
"{PLAN_PROMPT_WITH_SUMMARIES}",
@ -128,7 +128,7 @@ export async function generateAction(
const response = await modelWithTools.invoke([
{
role: "system",
content: formatPrompt(state, config),
content: formatPrompt(state),
},
...state.messages,
]);

View file

@ -83,11 +83,10 @@ export async function initialize(
}
}
const { target_repository } = config.configurable;
if (!target_repository) {
const { targetRepository } = state;
if (!targetRepository) {
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,
);
const res = await cloneRepo(sandbox, target_repository);
const res = await cloneRepo(sandbox, targetRepository);
if (res.error) {
// TODO: This should probably be an interrupt.
logger.error("Failed to clone repository", res.error);
@ -105,7 +104,7 @@ export async function initialize(
}
logger.info("Repository cloned successfully.");
const absoluteRepoDir = getRepoAbsolutePath(config);
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
logger.info(`Configuring git user for repository at "${absoluteRepoDir}"...`);
await configureGitUserInRepo(absoluteRepoDir, sandbox);
@ -125,5 +124,6 @@ export async function initialize(
return {
sandboxSessionId: sandbox.sandboxId,
targetRepository,
};
}

View file

@ -68,7 +68,7 @@ export async function openPullRequest(
const sandbox = await Sandbox.connect(sandboxSessionId);
const { owner, repo } = config.configurable?.target_repository ?? {};
const { owner, repo } = state.targetRepository;
if (!owner || !repo) {
throw new Error(
@ -77,7 +77,7 @@ export async function openPullRequest(
}
const changedFiles = await getChangedFilesStatus(
getRepoAbsolutePath(config),
getRepoAbsolutePath(state.targetRepository),
sandbox,
);
let branchName = state.branchName;
@ -85,9 +85,14 @@ export async function openPullRequest(
logger.info(`Has ${changedFiles.length} changed files. Committing.`, {
changedFiles,
});
branchName = await checkoutBranchAndCommit(config, sandbox, {
branchName,
});
branchName = await checkoutBranchAndCommit(
config,
state.targetRepository,
sandbox,
{
branchName,
},
);
}
const model = await loadModel(config, Task.SUMMARIZER);

View file

@ -129,7 +129,7 @@ export async function takeAction(
// If there are, commit them.
const sandbox = await Sandbox.connect(state.sandboxSessionId);
const changedFiles = await getChangedFilesStatus(
getRepoAbsolutePath(config),
getRepoAbsolutePath(state.targetRepository),
sandbox,
);
@ -138,9 +138,14 @@ export async function takeAction(
logger.info(`Has ${changedFiles.length} changed files. Committing.`, {
changedFiles,
});
branchName = await checkoutBranchAndCommit(config, sandbox, {
branchName,
});
branchName = await checkoutBranchAndCommit(
config,
state.targetRepository,
sandbox,
{
branchName,
},
);
}
const shouldRouteDiagnoseNode = shouldDiagnoseError(

View file

@ -73,6 +73,12 @@ export const GraphAnnotation = z.object({
.string()
.optional()
.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>;
@ -143,25 +149,6 @@ const MODEL_OPTIONS_NO_THINKING = MODEL_OPTIONS.filter(
);
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.
* This includes initial planning, and rewriting.

View file

@ -1,14 +1,16 @@
import { Octokit } from "@octokit/rest";
import { CommandResult, Sandbox } from "@e2b/code-interpreter";
import { createLogger, LogLevel } from "../logger.js";
import { GraphConfig } from "../../types.js";
import { GraphConfig, TargetRepository } from "../../types.js";
import { TIMEOUT_MS } from "../../constants.js";
import { getSandboxErrorFields } from "../sandbox-error-fields.js";
const logger = createLogger(LogLevel.INFO, "GitUtil");
export function getRepoAbsolutePath(config: GraphConfig): string {
const repoName = config.configurable?.target_repository.repo;
export function getRepoAbsolutePath(
targetRepository: TargetRepository,
): string {
const repoName = targetRepository.repo;
if (!repoName) {
throw new Error("No repository name provided");
}
@ -374,13 +376,14 @@ export async function getChangedFilesStatus(
export async function checkoutBranchAndCommit(
config: GraphConfig,
targetRepository: TargetRepository,
sandbox: Sandbox,
options?: {
branchName?: string;
},
): Promise<string> {
logger.info("Checking out branch and committing changes...");
const absoluteRepoDir = getRepoAbsolutePath(config);
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
const branchName = options?.branchName || getBranchName(config);
await checkoutBranch(absoluteRepoDir, branchName, sandbox);

View file

@ -89,13 +89,6 @@ export default function useInterruptedActions({
resume: response,
},
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,
},
},

View file

@ -215,8 +215,16 @@ export function Thread() {
const context =
Object.keys(artifactContext).length > 0 ? artifactContext : undefined;
const targetRepository = {
owner: "langchain-ai",
repo: "open-swe",
};
stream.submit(
{ messages: [...toolMessages, newHumanMessage], context },
{
messages: [...toolMessages, newHumanMessage],
context,
targetRepository,
},
{
streamMode: ["values"],
optimisticValues: (prev) => ({
@ -229,13 +237,6 @@ export function Thread() {
],
}),
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,
},
},
@ -255,13 +256,6 @@ export function Thread() {
checkpoint: parentCheckpoint,
streamMode: ["values"],
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,
},
});

View file

@ -68,13 +68,6 @@ export function HumanMessage({
};
},
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,
},
},

View file

@ -15,7 +15,12 @@ import { TooltipIconButton } from "@/components/thread/tooltip-icon-button";
import { Copy, CopyCheck } from "lucide-react";
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<
StateType,
@ -24,6 +29,7 @@ const useTypedStream = useStream<
messages?: Message[] | Message | string;
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
context?: Record<string, unknown>;
targetRepository?: TargetRepository;
};
CustomEventType: UIMessage | RemoveUIMessage;
}