mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-05 06:02:15 +00:00
feat: Evaluations to test open-swe's langGraph py ability (#385)
This commit is contained in:
parent
72a129f691
commit
1801fe7023
18 changed files with 1951 additions and 375 deletions
7
apps/open-swe/eval.tsconfig.json
Normal file
7
apps/open-swe/eval.tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["./evals/**/*.ts"]
|
||||||
|
}
|
||||||
274
apps/open-swe/evals/evaluator.ts
Normal file
274
apps/open-swe/evals/evaluator.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
import "dotenv/config";
|
||||||
|
import { OpenSWEInput, CodeTestDetails } from "./open-swe-types.js";
|
||||||
|
import { Daytona, Sandbox } from "@daytonaio/sdk";
|
||||||
|
import { createLogger, LogLevel } from "../src/utils/logger.js";
|
||||||
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
|
import { DEFAULT_SANDBOX_CREATE_PARAMS } from "../src/constants.js";
|
||||||
|
import { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { cloneRepo } from "../src/utils/github/git.js";
|
||||||
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
|
import { SimpleEvaluationResult } from "langsmith/vitest";
|
||||||
|
import { runRuffLint, runMyPyTypeCheck } from "./tests.js";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.INFO, "Evaluator ");
|
||||||
|
|
||||||
|
const VENV_PATH = ".venv";
|
||||||
|
const RUN_PYTHON_IN_VENV = `${VENV_PATH}/bin/python`;
|
||||||
|
const RUN_PIP_IN_VENV = `${VENV_PATH}/bin/pip`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Python environment with requirements.txt + ruff + mypy
|
||||||
|
*/
|
||||||
|
async function setupEnv(
|
||||||
|
sandbox: Sandbox,
|
||||||
|
absoluteRepoDir: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
logger.info("Setting up Python environment...");
|
||||||
|
|
||||||
|
const createVenvCommand = "python -m venv .venv";
|
||||||
|
const createVenvRes = await sandbox.process.executeCommand(
|
||||||
|
createVenvCommand,
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC,
|
||||||
|
);
|
||||||
|
if (createVenvRes.exitCode !== 0) {
|
||||||
|
logger.error("Failed to create virtual environment", {
|
||||||
|
createVenvCommand,
|
||||||
|
createVenvRes,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const upgradePipRes = await sandbox.process.executeCommand(
|
||||||
|
`${RUN_PIP_IN_VENV} install --upgrade pip`,
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC,
|
||||||
|
);
|
||||||
|
if (upgradePipRes.exitCode !== 0) {
|
||||||
|
logger.warn("Failed to upgrade pip, continuing anyway", { upgradePipRes });
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirementsExistRes = await sandbox.process.executeCommand(
|
||||||
|
"test -f requirements.txt",
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requirementsExistRes.exitCode === 0) {
|
||||||
|
logger.info("Found requirements.txt, installing...");
|
||||||
|
const installReqRes = await sandbox.process.executeCommand(
|
||||||
|
`${RUN_PIP_IN_VENV} install -r requirements.txt`,
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC * 3,
|
||||||
|
);
|
||||||
|
if (installReqRes.exitCode !== 0) {
|
||||||
|
logger.warn("Failed to install requirements.txt, continuing anyway", {
|
||||||
|
installReqRes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("No requirements.txt found, skipping repository dependencies");
|
||||||
|
}
|
||||||
|
|
||||||
|
const installAnalysisToolsRes = await sandbox.process.executeCommand(
|
||||||
|
`${RUN_PIP_IN_VENV} install ruff mypy`,
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC,
|
||||||
|
);
|
||||||
|
if (installAnalysisToolsRes.exitCode !== 0) {
|
||||||
|
logger.error("Failed to install ruff and mypy", {
|
||||||
|
installAnalysisToolsRes,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Environment setup completed successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs ruff and mypy analysis on all Python files in the repository
|
||||||
|
*/
|
||||||
|
async function runCodeTests(
|
||||||
|
sandbox: Sandbox,
|
||||||
|
absoluteRepoDir: string,
|
||||||
|
): Promise<{ ruffScore: number; mypyScore: number; details: CodeTestDetails }> {
|
||||||
|
logger.info("Running code analysis on all Python files in repository");
|
||||||
|
|
||||||
|
const testResults: {
|
||||||
|
ruffScore: number;
|
||||||
|
mypyScore: number;
|
||||||
|
details: CodeTestDetails;
|
||||||
|
} = {
|
||||||
|
ruffScore: 0,
|
||||||
|
mypyScore: 0,
|
||||||
|
details: {
|
||||||
|
ruff: {
|
||||||
|
issues: [],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
mypy: {
|
||||||
|
issues: [],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [ruffLint, mypyCheck] = await Promise.all([
|
||||||
|
runRuffLint(sandbox, {
|
||||||
|
command: `${RUN_PYTHON_IN_VENV} -m ruff check . --output-format=json`,
|
||||||
|
workingDir: absoluteRepoDir,
|
||||||
|
env: undefined,
|
||||||
|
timeoutSec: TIMEOUT_SEC * 3,
|
||||||
|
}),
|
||||||
|
runMyPyTypeCheck(sandbox, {
|
||||||
|
command: `${RUN_PYTHON_IN_VENV} -m mypy . --no-error-summary --show-error-codes --no-color-output`,
|
||||||
|
workingDir: absoluteRepoDir,
|
||||||
|
env: undefined,
|
||||||
|
timeoutSec: TIMEOUT_SEC * 3,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Object.assign(testResults, {
|
||||||
|
ruffScore: ruffLint.ruffScore,
|
||||||
|
mypyScore: mypyCheck.mypyScore,
|
||||||
|
details: {
|
||||||
|
ruff: {
|
||||||
|
issues: ruffLint.issues,
|
||||||
|
error: ruffLint.error,
|
||||||
|
},
|
||||||
|
mypy: {
|
||||||
|
issues: mypyCheck.issues,
|
||||||
|
error: mypyCheck.error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Code tests completed", {
|
||||||
|
ruffScore: testResults.ruffScore,
|
||||||
|
mypyScore: testResults.mypyScore,
|
||||||
|
ruffIssues: testResults.details.ruff.issues.length,
|
||||||
|
mypyIssues: testResults.details.mypy.issues.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return testResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main evaluator function for OpenSWE code analysis
|
||||||
|
*/
|
||||||
|
export async function evaluator(inputs: {
|
||||||
|
openSWEInputs: OpenSWEInput;
|
||||||
|
output: {
|
||||||
|
branchName: string;
|
||||||
|
targetRepository: TargetRepository;
|
||||||
|
};
|
||||||
|
}): Promise<SimpleEvaluationResult[]> {
|
||||||
|
const { openSWEInputs, output } = inputs;
|
||||||
|
|
||||||
|
const githubToken = process.env.GITHUB_PAT;
|
||||||
|
if (!githubToken) {
|
||||||
|
throw new Error("GITHUB_PAT environment variable is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const daytonaInstance = new Daytona();
|
||||||
|
logger.info("Creating sandbox...", {
|
||||||
|
repo: openSWEInputs.repo,
|
||||||
|
originalBranch: openSWEInputs.branch,
|
||||||
|
solutionBranch: output.branchName,
|
||||||
|
user_input: openSWEInputs.user_input.substring(0, 100) + "...",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sandbox = await daytonaInstance.create(DEFAULT_SANDBOX_CREATE_PARAMS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await cloneRepo(sandbox, output.targetRepository, {
|
||||||
|
githubInstallationToken: githubToken,
|
||||||
|
});
|
||||||
|
if (res.exitCode !== 0) {
|
||||||
|
logger.error("Failed to clone repository", {
|
||||||
|
targetRepository: output.targetRepository,
|
||||||
|
cloneResult: res,
|
||||||
|
});
|
||||||
|
throw new Error("Failed to clone repository");
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteRepoDir = getRepoAbsolutePath(output.targetRepository);
|
||||||
|
|
||||||
|
const solutionBranch = output.branchName;
|
||||||
|
logger.info(`Checking out agent's solution branch: ${solutionBranch}`);
|
||||||
|
|
||||||
|
const checkoutBranchRes = await sandbox.process.executeCommand(
|
||||||
|
`git checkout ${solutionBranch}`,
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC,
|
||||||
|
);
|
||||||
|
if (checkoutBranchRes.exitCode !== 0) {
|
||||||
|
logger.error("Failed to checkout solution branch", {
|
||||||
|
solutionBranch,
|
||||||
|
checkoutResult: checkoutBranchRes,
|
||||||
|
});
|
||||||
|
throw new Error(`Failed to checkout solution branch: ${solutionBranch}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envSetupSuccess = await setupEnv(sandbox, absoluteRepoDir);
|
||||||
|
if (!envSetupSuccess) {
|
||||||
|
logger.error("Failed to setup environment");
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "overall-score",
|
||||||
|
score: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysisResult = await runCodeTests(sandbox, absoluteRepoDir);
|
||||||
|
|
||||||
|
const overallScore = analysisResult.ruffScore + analysisResult.mypyScore;
|
||||||
|
|
||||||
|
logger.info("Evaluation completed", {
|
||||||
|
overallScore,
|
||||||
|
ruffScore: analysisResult.ruffScore,
|
||||||
|
mypyScore: analysisResult.mypyScore,
|
||||||
|
repo: openSWEInputs.repo,
|
||||||
|
originalBranch: openSWEInputs.branch,
|
||||||
|
solutionBranch: output.branchName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "overall-score",
|
||||||
|
score: overallScore,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ruff-score",
|
||||||
|
score: analysisResult.ruffScore,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mypy-score",
|
||||||
|
score: analysisResult.mypyScore,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Evaluation failed with error", { error });
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "overall-score",
|
||||||
|
score: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await sandbox.delete();
|
||||||
|
logger.info("Sandbox cleaned up successfully");
|
||||||
|
} catch (cleanupError) {
|
||||||
|
logger.error("Failed to cleanup sandbox", { cleanupError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
235
apps/open-swe/evals/langgraph.eval.ts
Normal file
235
apps/open-swe/evals/langgraph.eval.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
// Run evals over the development Open SWE dataset
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import * as ls from "langsmith/vitest";
|
||||||
|
import { formatInputs } from "./prompts.js";
|
||||||
|
import { createLogger, LogLevel } from "../src/utils/logger.js";
|
||||||
|
import { evaluator } from "./evaluator.js";
|
||||||
|
import { MANAGER_GRAPH_ID, GITHUB_PAT } from "@open-swe/shared/constants";
|
||||||
|
import { createLangGraphClient } from "../src/utils/langgraph-client.js";
|
||||||
|
import { encryptSecret } from "@open-swe/shared/crypto";
|
||||||
|
import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
|
||||||
|
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
||||||
|
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { withRetry } from "./utils/retry.js";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.DEBUG, "Evaluator");
|
||||||
|
|
||||||
|
const DATASET_NAME = process.env.DATASET_NAME || "";
|
||||||
|
// const RUN_NAME = `${DATASET_NAME}-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
||||||
|
|
||||||
|
// async function loadDataset(): Promise<Example[]> {
|
||||||
|
// const client = new LangSmithClient();
|
||||||
|
// const datasetStream = client.listExamples({ datasetName: DATASET_NAME });
|
||||||
|
// let examples: Example[] = [];
|
||||||
|
// for await (const example of datasetStream) {
|
||||||
|
// examples.push(example);
|
||||||
|
// }
|
||||||
|
// logger.info(
|
||||||
|
// `Loaded ${examples.length} examples from dataset "${DATASET_NAME}"`,
|
||||||
|
// );
|
||||||
|
// return examples;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const DATASET = await loadDataset().then((examples) =>
|
||||||
|
// examples.map(example => ({
|
||||||
|
// inputs: example.inputs as OpenSWEInput,
|
||||||
|
// })),
|
||||||
|
// );
|
||||||
|
|
||||||
|
const DATASET = [
|
||||||
|
{
|
||||||
|
inputs: {
|
||||||
|
repo: "mai-sandbox/open-swe_content_team_eval",
|
||||||
|
branch: "main",
|
||||||
|
user_input: `I have implemented a multi-agent content creation system using LangGraph that orchestrates collaboration between specialized agents. The system is experiencing multiple runtime errors and workflow failures that prevent proper execution.
|
||||||
|
|
||||||
|
System Architecture
|
||||||
|
The application implements a three-agent architecture:
|
||||||
|
|
||||||
|
Research Agent: Utilizes web search tools to gather information on specified topics
|
||||||
|
Writer Agent: Creates content based on research findings with creative temperature settings
|
||||||
|
Reviewer Agent: Provides feedback using fact-checking tools and determines revision needs
|
||||||
|
|
||||||
|
Expected Workflow
|
||||||
|
User Request → Research Agent → Writer Agent → Reviewer Agent → [Revision Loop if needed] → Final Content
|
||||||
|
|
||||||
|
Current Issues
|
||||||
|
|
||||||
|
Runtime Errors: Application fails to start with import and graph compilation errors
|
||||||
|
Agent Handoff Failures: Agents are not properly transferring control and context
|
||||||
|
Tool Integration Problems: Tool calling mechanisms are not functioning correctly
|
||||||
|
State Management Issues: Shared state is not being updated correctly across agent transitions
|
||||||
|
Routing Logic Failures: Conditional edges and workflow routing are broken`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.info(`Starting evals over ${DATASET.length} examples...`);
|
||||||
|
|
||||||
|
//const LANGGRAPH_URL = process.env.LANGGRAPH_URL || "http://localhost:2024";
|
||||||
|
|
||||||
|
ls.describe(DATASET_NAME, () => {
|
||||||
|
ls.test.each(DATASET)(
|
||||||
|
"Can resolve issue",
|
||||||
|
async ({ inputs }) => {
|
||||||
|
logger.info("Starting agent run", {
|
||||||
|
inputs,
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||||
|
const githubPat = process.env.GITHUB_PAT;
|
||||||
|
|
||||||
|
if (!encryptionKey || !githubPat) {
|
||||||
|
throw new Error(
|
||||||
|
"SECRETS_ENCRYPTION_KEY and GITHUB_PAT environment variables are required",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedGitHubToken = encryptSecret(githubPat, encryptionKey);
|
||||||
|
|
||||||
|
const lgClient = createLangGraphClient({
|
||||||
|
includeApiKey: true,
|
||||||
|
defaultHeaders: { [GITHUB_PAT]: encryptedGitHubToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = await formatInputs(inputs);
|
||||||
|
|
||||||
|
const threadId = uuidv4();
|
||||||
|
logger.info("Starting agent run", {
|
||||||
|
thread_id: threadId,
|
||||||
|
problem: inputs.user_input,
|
||||||
|
repo: inputs.repo,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the agent with user input
|
||||||
|
let managerRun;
|
||||||
|
try {
|
||||||
|
managerRun = await withRetry(() =>
|
||||||
|
lgClient.runs.wait(threadId, MANAGER_GRAPH_ID, {
|
||||||
|
input,
|
||||||
|
config: {
|
||||||
|
recursion_limit: 250,
|
||||||
|
},
|
||||||
|
ifNotExists: "create",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in manager run", {
|
||||||
|
thread_id: threadId,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
name: error.name,
|
||||||
|
cause: error.cause,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
});
|
||||||
|
return; // instead of skipping, we should award 0 points
|
||||||
|
}
|
||||||
|
|
||||||
|
const managerState = managerRun as unknown as ManagerGraphState;
|
||||||
|
const plannerSession = managerState?.plannerSession;
|
||||||
|
|
||||||
|
if (!plannerSession) {
|
||||||
|
logger.info("Agent did not create a planner session", {
|
||||||
|
thread_id: threadId,
|
||||||
|
});
|
||||||
|
return; // instead of skipping, we should award 0 points
|
||||||
|
}
|
||||||
|
|
||||||
|
let plannerRun;
|
||||||
|
try {
|
||||||
|
plannerRun = await withRetry(() =>
|
||||||
|
lgClient.runs.join(plannerSession.threadId, plannerSession.runId),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error joining planner run", {
|
||||||
|
thread_id: threadId,
|
||||||
|
plannerSession,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
name: error.name,
|
||||||
|
cause: error.cause,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
});
|
||||||
|
return; // instead of skipping, we should award 0 points
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe access to planner run state
|
||||||
|
const plannerState = plannerRun as unknown as PlannerGraphState;
|
||||||
|
const programmerSession = plannerState?.programmerSession;
|
||||||
|
|
||||||
|
if (!programmerSession) {
|
||||||
|
logger.info("Agent did not create a programmer session", {
|
||||||
|
thread_id: threadId,
|
||||||
|
});
|
||||||
|
return; // instead of skipping, we should award 0 points
|
||||||
|
}
|
||||||
|
|
||||||
|
let programmerRun;
|
||||||
|
try {
|
||||||
|
programmerRun = await withRetry(() =>
|
||||||
|
lgClient.runs.join(
|
||||||
|
programmerSession.threadId,
|
||||||
|
programmerSession.runId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error joining programmer run", {
|
||||||
|
thread_id: threadId,
|
||||||
|
programmerSession,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
name: error.name,
|
||||||
|
cause: error.cause,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
});
|
||||||
|
return; // instead of skipping, we should award 0 points
|
||||||
|
}
|
||||||
|
|
||||||
|
const programmerState = programmerRun as unknown as GraphState;
|
||||||
|
const branchName = programmerState?.branchName;
|
||||||
|
|
||||||
|
if (!branchName) {
|
||||||
|
logger.info("Agent did not create a branch", {
|
||||||
|
thread_id: threadId,
|
||||||
|
});
|
||||||
|
return; // instead of skipping, we should award 0 points
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Agent completed. Created branch:", {
|
||||||
|
branchName: branchName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evaluation
|
||||||
|
const wrappedEvaluator = ls.wrapEvaluator(evaluator);
|
||||||
|
const evalResult = await wrappedEvaluator({
|
||||||
|
openSWEInputs: inputs,
|
||||||
|
output: {
|
||||||
|
branchName,
|
||||||
|
targetRepository: {
|
||||||
|
owner: inputs.repo.split("/")[0],
|
||||||
|
repo: inputs.repo.split("/")[1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Evaluation completed.", {
|
||||||
|
thread_id: threadId,
|
||||||
|
evalResult,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
7200_000,
|
||||||
|
);
|
||||||
|
});
|
||||||
104
apps/open-swe/evals/open-swe-types.ts
Normal file
104
apps/open-swe/evals/open-swe-types.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
/**
|
||||||
|
* Input structure for Open SWE evaluations
|
||||||
|
* This is much simpler than SWE-Bench since we only need
|
||||||
|
* problem statement + repo info for ruff/mypy analysis
|
||||||
|
*/
|
||||||
|
export interface OpenSWEInput {
|
||||||
|
/**
|
||||||
|
* The user request/problem statement that was given to Open SWE
|
||||||
|
* This is what gets passed to the agent to solve
|
||||||
|
*/
|
||||||
|
user_input: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository information in "owner/repo" format
|
||||||
|
* e.g., "aliyanishfaq/my-project"
|
||||||
|
*/
|
||||||
|
repo: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: Branch name where the agent's solution is located
|
||||||
|
* If not provided, agent will create one (e.g., "open-swe/uuid")
|
||||||
|
*/
|
||||||
|
branch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process execution options
|
||||||
|
*/
|
||||||
|
export interface ExecOptions {
|
||||||
|
command: string;
|
||||||
|
workingDir: string;
|
||||||
|
env: Record<string, string> | undefined;
|
||||||
|
timeoutSec: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruff issue location
|
||||||
|
*/
|
||||||
|
export interface RuffLocation {
|
||||||
|
column: number;
|
||||||
|
row: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruff fix edit
|
||||||
|
*/
|
||||||
|
export interface RuffEdit {
|
||||||
|
content: string;
|
||||||
|
end_location: RuffLocation;
|
||||||
|
location: RuffLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruff fix suggestion
|
||||||
|
*/
|
||||||
|
export interface RuffFix {
|
||||||
|
applicability: "safe" | "unsafe" | "display";
|
||||||
|
edits: RuffEdit[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual Ruff issue
|
||||||
|
*/
|
||||||
|
export interface RuffIssue {
|
||||||
|
cell: string | null;
|
||||||
|
code: string;
|
||||||
|
end_location: RuffLocation;
|
||||||
|
filename: string;
|
||||||
|
fix: RuffFix | null;
|
||||||
|
location: RuffLocation;
|
||||||
|
message: string;
|
||||||
|
noqa_row: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return type for ruffPromise function
|
||||||
|
*/
|
||||||
|
export interface RuffResult {
|
||||||
|
ruffScore: number;
|
||||||
|
error: Error | null;
|
||||||
|
issues: RuffIssue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return type for mypyPromise function
|
||||||
|
*/
|
||||||
|
export interface MyPyResult {
|
||||||
|
mypyScore: number;
|
||||||
|
error: Error | null;
|
||||||
|
issues: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeTestDetails {
|
||||||
|
ruff: {
|
||||||
|
issues: RuffIssue[];
|
||||||
|
error: Error | null;
|
||||||
|
};
|
||||||
|
mypy: {
|
||||||
|
issues: string[];
|
||||||
|
error: Error | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
60
apps/open-swe/evals/prompts.ts
Normal file
60
apps/open-swe/evals/prompts.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { OpenSWEInput } from "./open-swe-types.js";
|
||||||
|
import { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { HumanMessage } from "@langchain/core/messages";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types";
|
||||||
|
|
||||||
|
async function getRepoReadmeContents(
|
||||||
|
targetRepository: TargetRepository,
|
||||||
|
): Promise<string> {
|
||||||
|
if (!process.env.GITHUB_PAT) {
|
||||||
|
throw new Error("GITHUB_PAT environment variable missing.");
|
||||||
|
}
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: process.env.GITHUB_PAT,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await octokit.repos.getReadme({
|
||||||
|
owner: targetRepository.owner,
|
||||||
|
repo: targetRepository.repo,
|
||||||
|
});
|
||||||
|
return Buffer.from(data.content, "base64").toString("utf-8");
|
||||||
|
} catch (_) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function formatInputs(
|
||||||
|
inputs: OpenSWEInput,
|
||||||
|
): Promise<ManagerGraphUpdate> {
|
||||||
|
const targetRepository: TargetRepository = {
|
||||||
|
owner: inputs.repo.split("/")[0],
|
||||||
|
repo: inputs.repo.split("/")[1],
|
||||||
|
branch: inputs.branch,
|
||||||
|
};
|
||||||
|
|
||||||
|
const readmeContents = await getRepoReadmeContents(targetRepository);
|
||||||
|
|
||||||
|
const SIMPLE_PROMPT_TEMPLATE = `<request>
|
||||||
|
{USER_REQUEST}
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<codebase-readme>
|
||||||
|
{CODEBASE_README}
|
||||||
|
</codebase-readme>`;
|
||||||
|
|
||||||
|
const userMessageContent = SIMPLE_PROMPT_TEMPLATE.replace(
|
||||||
|
"{REPO}",
|
||||||
|
inputs.repo,
|
||||||
|
)
|
||||||
|
.replace("{USER_REQUEST}", inputs.user_input)
|
||||||
|
.replace("{CODEBASE_README}", readmeContents);
|
||||||
|
|
||||||
|
const userMessage = new HumanMessage(userMessageContent);
|
||||||
|
return {
|
||||||
|
messages: [userMessage],
|
||||||
|
targetRepository,
|
||||||
|
autoAcceptPlan: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
132
apps/open-swe/evals/tests.ts
Normal file
132
apps/open-swe/evals/tests.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
// TODO: Add ruff promise and the mypy promise to the tests.
|
||||||
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
|
import { createLogger, LogLevel } from "../src/utils/logger.js";
|
||||||
|
import {
|
||||||
|
ExecOptions,
|
||||||
|
RuffResult,
|
||||||
|
RuffIssue,
|
||||||
|
MyPyResult,
|
||||||
|
} from "./open-swe-types.js";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.DEBUG, " Evaluation Tests");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run ruff check and return score, error, and issues
|
||||||
|
*/
|
||||||
|
export const runRuffLint = async (
|
||||||
|
sandbox: Sandbox,
|
||||||
|
args: ExecOptions,
|
||||||
|
): Promise<RuffResult> => {
|
||||||
|
logger.info("Running ruff check...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const execution = await sandbox.process.executeCommand(
|
||||||
|
args.command,
|
||||||
|
args.workingDir,
|
||||||
|
args.env,
|
||||||
|
args.timeoutSec,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (execution.exitCode === 0) {
|
||||||
|
logger.info("Ruff analysis passed. No issues found.");
|
||||||
|
return {
|
||||||
|
ruffScore: 1,
|
||||||
|
error: null,
|
||||||
|
issues: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issues: RuffIssue[] = JSON.parse(execution.result);
|
||||||
|
const issueCount = Array.isArray(issues) ? issues.length : 0;
|
||||||
|
const ruffScore = issueCount === 0 ? 1 : 0;
|
||||||
|
|
||||||
|
logger.info(`Ruff found ${issueCount} issues`, {
|
||||||
|
score: ruffScore,
|
||||||
|
issues: issues.slice(0, 3), // Log first 3 issues
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ruffScore,
|
||||||
|
error: null,
|
||||||
|
issues,
|
||||||
|
};
|
||||||
|
} catch (parseError) {
|
||||||
|
logger.warn(
|
||||||
|
"Could not parse ruff JSON output. Setting Ruff score to 0.",
|
||||||
|
{
|
||||||
|
parseError,
|
||||||
|
output: execution.result?.substring(0, 200) + "...",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ruffScore: 0,
|
||||||
|
error: parseError as Error,
|
||||||
|
issues: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to run ruff check", { error });
|
||||||
|
return {
|
||||||
|
ruffScore: 0,
|
||||||
|
error: error as Error,
|
||||||
|
issues: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run mypy check and return score, error, and issues
|
||||||
|
*/
|
||||||
|
export const runMyPyTypeCheck = async (
|
||||||
|
sandbox: Sandbox,
|
||||||
|
args: ExecOptions,
|
||||||
|
): Promise<MyPyResult> => {
|
||||||
|
logger.info("Running mypy check...");
|
||||||
|
try {
|
||||||
|
const execution = await sandbox.process.executeCommand(
|
||||||
|
args.command,
|
||||||
|
args.workingDir,
|
||||||
|
args.env,
|
||||||
|
args.timeoutSec,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (execution.exitCode === 0) {
|
||||||
|
logger.info("Mypy analysis passed. No issues found.");
|
||||||
|
return {
|
||||||
|
mypyScore: 1,
|
||||||
|
error: null,
|
||||||
|
issues: [],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Filter for actual type problems: errors and warnings
|
||||||
|
const errorLines = execution.result
|
||||||
|
.split("\n")
|
||||||
|
.filter(
|
||||||
|
(line) => line.includes(": error:") || line.includes(": warning:"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const issueCount = errorLines.length;
|
||||||
|
const mypyScore = issueCount === 0 ? 1 : 0;
|
||||||
|
|
||||||
|
logger.info(`Mypy found ${issueCount} issues`, {
|
||||||
|
score: mypyScore,
|
||||||
|
issues: errorLines.slice(0, 3),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
mypyScore,
|
||||||
|
error: null,
|
||||||
|
issues: errorLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to run mypy check", { error });
|
||||||
|
return {
|
||||||
|
mypyScore: 0,
|
||||||
|
error: error as Error,
|
||||||
|
issues: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
51
apps/open-swe/evals/utils/retry.ts
Normal file
51
apps/open-swe/evals/utils/retry.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { createLogger, LogLevel } from "../../src/utils/logger.js";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.DEBUG, "Retry");
|
||||||
|
|
||||||
|
const RETRY_CONFIG = {
|
||||||
|
maxRetries: 5,
|
||||||
|
baseDelay: 1000,
|
||||||
|
maxDelay: 30000,
|
||||||
|
backoffMultiplier: 2,
|
||||||
|
timeoutErrors: ["UND_ERR_HEADERS_TIMEOUT"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry decorator with exponential backoff for LangGraph client
|
||||||
|
* operations.
|
||||||
|
*/
|
||||||
|
export async function withRetry<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
|
let lastError: any;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < RETRY_CONFIG.maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
const isRetryable = RETRY_CONFIG.timeoutErrors.includes(
|
||||||
|
error?.cause?.code,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRetryable && attempt < RETRY_CONFIG.maxRetries - 1) {
|
||||||
|
const delay = Math.min(
|
||||||
|
RETRY_CONFIG.baseDelay *
|
||||||
|
Math.pow(RETRY_CONFIG.backoffMultiplier, attempt),
|
||||||
|
RETRY_CONFIG.maxDelay,
|
||||||
|
);
|
||||||
|
logger.info(
|
||||||
|
`Retrying operation in ${delay}ms. Attempt ${attempt + 1} of ${RETRY_CONFIG.maxRetries}`,
|
||||||
|
{
|
||||||
|
attempt,
|
||||||
|
lastError,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
} else {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
13
apps/open-swe/ls.vitest.config.ts
Normal file
13
apps/open-swe/ls.vitest.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["**/*.eval.?(c|m)[jt]s"],
|
||||||
|
reporters: ["langsmith/vitest/reporter"],
|
||||||
|
setupFiles: ["dotenv/config"],
|
||||||
|
typecheck: {
|
||||||
|
tsconfig: "./eval.tsconfig.json",
|
||||||
|
},
|
||||||
|
testTimeout: 7200_000, // 120 minutes
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
|
||||||
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
|
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
|
||||||
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testTimeout 100000",
|
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testTimeout 100000",
|
||||||
|
"eval:single": "NODE_OPTIONS=--experimental-vm-modules yarn run vitest --config ls.vitest.config.ts --run",
|
||||||
"postinstall": "turbo build"
|
"postinstall": "turbo build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -63,7 +64,8 @@
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"turbo": "^2.5.0",
|
"turbo": "^2.5.0",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.22.0"
|
"typescript-eslint": "^8.22.0",
|
||||||
|
"vitest": "^3.2.3"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.5.1"
|
"packageManager": "yarn@3.5.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,20 @@ export const DEFAULT_SANDBOX_CREATE_PARAMS: CreateSandboxParams = {
|
||||||
user: "daytona",
|
user: "daytona",
|
||||||
image: SNAPSHOT_NAME,
|
image: SNAPSHOT_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LANGGRAPH_USER_PERMISSIONS = [
|
||||||
|
"threads:create",
|
||||||
|
"threads:create_run",
|
||||||
|
"threads:read",
|
||||||
|
"threads:delete",
|
||||||
|
"threads:update",
|
||||||
|
"threads:search",
|
||||||
|
"assistants:create",
|
||||||
|
"assistants:read",
|
||||||
|
"assistants:delete",
|
||||||
|
"assistants:update",
|
||||||
|
"assistants:search",
|
||||||
|
"deployments:read",
|
||||||
|
"deployments:search",
|
||||||
|
"store:access",
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import {
|
||||||
import { decryptSecret } from "@open-swe/shared/crypto";
|
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||||
import { verifyGitHubWebhookOrThrow } from "./github.js";
|
import { verifyGitHubWebhookOrThrow } from "./github.js";
|
||||||
import { createWithOwnerMetadata, createOwnerFilter } from "./utils.js";
|
import { createWithOwnerMetadata, createOwnerFilter } from "./utils.js";
|
||||||
|
import { LANGGRAPH_USER_PERMISSIONS } from "../constants.js";
|
||||||
|
import { getGitHubPatFromRequest } from "../utils/github-pat.js";
|
||||||
|
|
||||||
// TODO: Export from LangGraph SDK
|
// TODO: Export from LangGraph SDK
|
||||||
export interface BaseAuthReturn {
|
export interface BaseAuthReturn {
|
||||||
|
|
@ -43,6 +45,8 @@ export const auth = new Auth()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
const ghSecretHashHeader = request.headers.get("X-Hub-Signature-256");
|
const ghSecretHashHeader = request.headers.get("X-Hub-Signature-256");
|
||||||
if (ghSecretHashHeader) {
|
if (ghSecretHashHeader) {
|
||||||
// This will either return a valid user, or throw an error
|
// This will either return a valid user, or throw an error
|
||||||
|
|
@ -54,6 +58,28 @@ export const auth = new Auth()
|
||||||
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for GitHub PAT authentication (simpler mode for evals, etc.)
|
||||||
|
const githubPat = getGitHubPatFromRequest(request, encryptionKey);
|
||||||
|
if (githubPat && !isProd) {
|
||||||
|
const user = await verifyGithubUser(githubPat);
|
||||||
|
if (!user) {
|
||||||
|
throw new HTTPException(401, {
|
||||||
|
message: "Invalid GitHub PAT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
identity: user.id.toString(),
|
||||||
|
is_authenticated: true,
|
||||||
|
display_name: user.login,
|
||||||
|
metadata: {
|
||||||
|
installation_name: "pat-auth",
|
||||||
|
},
|
||||||
|
permissions: LANGGRAPH_USER_PERMISSIONS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitHub App authentication mode (existing logic)
|
||||||
const installationNameHeader = request.headers.get(
|
const installationNameHeader = request.headers.get(
|
||||||
GITHUB_INSTALLATION_NAME,
|
GITHUB_INSTALLATION_NAME,
|
||||||
);
|
);
|
||||||
|
|
@ -112,22 +138,7 @@ export const auth = new Auth()
|
||||||
metadata: {
|
metadata: {
|
||||||
installation_name: installationNameHeader,
|
installation_name: installationNameHeader,
|
||||||
},
|
},
|
||||||
permissions: [
|
permissions: LANGGRAPH_USER_PERMISSIONS,
|
||||||
"threads:create",
|
|
||||||
"threads:create_run",
|
|
||||||
"threads:read",
|
|
||||||
"threads:delete",
|
|
||||||
"threads:update",
|
|
||||||
"threads:search",
|
|
||||||
"assistants:create",
|
|
||||||
"assistants:read",
|
|
||||||
"assistants:delete",
|
|
||||||
"assistants:update",
|
|
||||||
"assistants:search",
|
|
||||||
"deployments:read",
|
|
||||||
"deployments:search",
|
|
||||||
"store:access",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { HTTPException } from "@langchain/langgraph-sdk/auth";
|
import { HTTPException } from "@langchain/langgraph-sdk/auth";
|
||||||
import { Webhooks } from "@octokit/webhooks";
|
import { Webhooks } from "@octokit/webhooks";
|
||||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||||
|
import { LANGGRAPH_USER_PERMISSIONS } from "../constants.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GitHubWebhookAuth");
|
const logger = createLogger(LogLevel.INFO, "GitHubWebhookAuth");
|
||||||
|
|
||||||
|
|
@ -41,21 +42,6 @@ export async function verifyGitHubWebhookOrThrow(request: Request) {
|
||||||
metadata: {
|
metadata: {
|
||||||
installation_name: "n/a",
|
installation_name: "n/a",
|
||||||
},
|
},
|
||||||
permissions: [
|
permissions: LANGGRAPH_USER_PERMISSIONS,
|
||||||
"threads:create",
|
|
||||||
"threads:create_run",
|
|
||||||
"threads:read",
|
|
||||||
"threads:delete",
|
|
||||||
"threads:update",
|
|
||||||
"threads:search",
|
|
||||||
"assistants:create",
|
|
||||||
"assistants:read",
|
|
||||||
"assistants:delete",
|
|
||||||
"assistants:update",
|
|
||||||
"assistants:search",
|
|
||||||
"deployments:read",
|
|
||||||
"deployments:search",
|
|
||||||
"store:access",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,19 @@ import {
|
||||||
GITHUB_USER_ID_HEADER,
|
GITHUB_USER_ID_HEADER,
|
||||||
GITHUB_USER_LOGIN_HEADER,
|
GITHUB_USER_LOGIN_HEADER,
|
||||||
GITHUB_INSTALLATION_NAME,
|
GITHUB_INSTALLATION_NAME,
|
||||||
|
GITHUB_PAT,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
|
|
||||||
export function getDefaultHeaders(config: GraphConfig) {
|
export function getDefaultHeaders(config: GraphConfig): Record<string, string> {
|
||||||
|
const githubPat = config.configurable?.[GITHUB_PAT];
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
if (githubPat && !isProd) {
|
||||||
|
// PAT-only
|
||||||
|
return {
|
||||||
|
[GITHUB_PAT]: githubPat,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const githubInstallationTokenCookie =
|
const githubInstallationTokenCookie =
|
||||||
config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE];
|
config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE];
|
||||||
const githubInstallationName =
|
const githubInstallationName =
|
||||||
|
|
|
||||||
33
apps/open-swe/src/utils/github-pat.ts
Normal file
33
apps/open-swe/src/utils/github-pat.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { GITHUB_PAT } from "@open-swe/shared/constants";
|
||||||
|
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple helper to check if request has GitHub PAT and return decrypted value
|
||||||
|
*/
|
||||||
|
export function getGitHubPatFromRequest(
|
||||||
|
request: Request,
|
||||||
|
encryptionKey: string,
|
||||||
|
): string | null {
|
||||||
|
const encryptedGitHubPat = request.headers.get(GITHUB_PAT);
|
||||||
|
if (!encryptedGitHubPat) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptSecret(encryptedGitHubPat, encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check if configurable has GitHub PAT and return decrypted value
|
||||||
|
*/
|
||||||
|
export function getGitHubPatFromConfig(
|
||||||
|
configurable: Record<string, any> | undefined,
|
||||||
|
encryptionKey: string,
|
||||||
|
): string | null {
|
||||||
|
if (!configurable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const encryptedGitHubPat = configurable[GITHUB_PAT];
|
||||||
|
if (!encryptedGitHubPat) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decryptSecret(encryptedGitHubPat, encryptionKey);
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import { decryptSecret } from "@open-swe/shared/crypto";
|
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||||
|
import { getGitHubPatFromConfig } from "./github-pat.js";
|
||||||
|
|
||||||
export function getGitHubTokensFromConfig(config: GraphConfig): {
|
export function getGitHubTokensFromConfig(config: GraphConfig): {
|
||||||
githubAccessToken: string;
|
githubAccessToken: string;
|
||||||
|
|
@ -12,6 +13,24 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
|
||||||
if (!config.configurable) {
|
if (!config.configurable) {
|
||||||
throw new Error("No configurable object found in graph config.");
|
throw new Error("No configurable object found in graph config.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the encryption key from environment variables
|
||||||
|
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
const githubPat = getGitHubPatFromConfig(config.configurable, encryptionKey);
|
||||||
|
if (githubPat && !isProd) {
|
||||||
|
// check for PAT-only mode
|
||||||
|
return {
|
||||||
|
githubAccessToken: githubPat,
|
||||||
|
githubInstallationToken: githubPat,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const encryptedGitHubToken = config.configurable[GITHUB_TOKEN_COOKIE];
|
const encryptedGitHubToken = config.configurable[GITHUB_TOKEN_COOKIE];
|
||||||
const encryptedInstallationToken =
|
const encryptedInstallationToken =
|
||||||
config.configurable[GITHUB_INSTALLATION_TOKEN_COOKIE];
|
config.configurable[GITHUB_INSTALLATION_TOKEN_COOKIE];
|
||||||
|
|
@ -21,12 +40,6 @@ export function getGitHubTokensFromConfig(config: GraphConfig): {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the encryption key from environment variables
|
|
||||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
|
||||||
if (!encryptionKey) {
|
|
||||||
throw new Error("Missing SECRETS_ENCRYPTION_KEY environment variable.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt the GitHub token
|
// Decrypt the GitHub token
|
||||||
const githubAccessToken = encryptedGitHubToken
|
const githubAccessToken = encryptedGitHubToken
|
||||||
? decryptSecret(encryptedGitHubToken, encryptionKey)
|
? decryptSecret(encryptedGitHubToken, encryptionKey)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export const PLAN_INTERRUPT_ACTION_TITLE = "Approve/Edit Plan";
|
||||||
export const GITHUB_TOKEN_COOKIE = "x-github-access-token";
|
export const GITHUB_TOKEN_COOKIE = "x-github-access-token";
|
||||||
export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token";
|
export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token";
|
||||||
export const GITHUB_INSTALLATION_NAME = "x-github-installation-name";
|
export const GITHUB_INSTALLATION_NAME = "x-github-installation-name";
|
||||||
|
export const GITHUB_PAT = "x-github-pat";
|
||||||
|
|
||||||
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
|
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
|
||||||
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";
|
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
GITHUB_TOKEN_COOKIE,
|
GITHUB_TOKEN_COOKIE,
|
||||||
GITHUB_USER_ID_HEADER,
|
GITHUB_USER_ID_HEADER,
|
||||||
GITHUB_USER_LOGIN_HEADER,
|
GITHUB_USER_LOGIN_HEADER,
|
||||||
|
GITHUB_PAT,
|
||||||
DEFAULT_MCP_SERVERS,
|
DEFAULT_MCP_SERVERS,
|
||||||
} from "../constants.js";
|
} from "../constants.js";
|
||||||
import { withLangGraph } from "@langchain/langgraph/zod";
|
import { withLangGraph } from "@langchain/langgraph/zod";
|
||||||
|
|
@ -414,6 +415,11 @@ export const GraphConfigurationMetadata: {
|
||||||
type: "hidden",
|
type: "hidden",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[GITHUB_PAT]: {
|
||||||
|
x_open_swe_ui_config: {
|
||||||
|
type: "hidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
x_open_swe_ui_config: {
|
x_open_swe_ui_config: {
|
||||||
type: "json",
|
type: "json",
|
||||||
|
|
@ -598,6 +604,14 @@ export const GraphConfiguration = z.object({
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.langgraph.metadata(GraphConfigurationMetadata[GITHUB_INSTALLATION_NAME]),
|
.langgraph.metadata(GraphConfigurationMetadata[GITHUB_INSTALLATION_NAME]),
|
||||||
|
/**
|
||||||
|
* GitHub Personal Access Token. Used for simpler authentication in environments like evals
|
||||||
|
* where GitHub App installation tokens are not available or needed.
|
||||||
|
*/
|
||||||
|
[GITHUB_PAT]: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.langgraph.metadata(GraphConfigurationMetadata[GITHUB_PAT]),
|
||||||
/**
|
/**
|
||||||
* Custom MCP servers configuration as JSON string. Merges with default servers.
|
* Custom MCP servers configuration as JSON string. Merges with default servers.
|
||||||
* @default Default LangGraph docs MCP server
|
* @default Default LangGraph docs MCP server
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue