-
Notifications
You must be signed in to change notification settings - Fork 53
/
Copy pathsoftwareDeveloperAgent.ts
128 lines (109 loc) · 6.02 KB
/
softwareDeveloperAgent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { addNote, agentContext, getFileSystem } from '#agent/agentContextLocalStorage';
import { func, funcClass } from '#functionSchema/functionDecorators';
import { GitProject } from '#functions/scm/gitProject';
import { GitLabProject } from '#functions/scm/gitlab';
import { getSourceControlManagementTool } from '#functions/scm/sourceControlManagement';
import { logger } from '#o11y/logger';
import { span } from '#o11y/trace';
import { createBranchName } from '#swe/createBranchName';
import { generatePullRequestTitleDescription } from '#swe/pullRequestTitleDescription';
import { selectProject } from '#swe/selectProject';
import { summariseRequirements } from '#swe/summariseRequirements';
import { ExecResult, execCommand, failOnError, runShellCommand } from '#utils/exec';
import { cacheRetry } from '../cache/cacheRetry';
import { CodeEditingAgent } from './codeEditingAgent';
import { ProjectInfo, detectProjectInfo } from './projectDetection';
import { basePrompt } from './prompt';
export function buildPrompt(args: {
information: string;
requirements: string;
action: string;
}): string {
return `${basePrompt}\n\n${args.information}\n\nThe requirements of the task are as follows:\n<requirements>\n${args.requirements}\n</requirements>\n\nThe action to be performed is as follows:\n<action>\n${args.action}\n</action>\n`;
}
/**
* Workflow for completing requirements. This will look up the appropriate project in source control, clone, make the changes and create a pull/merge request.
* Assumes the SourceControlManagement tool is set on the workflow context
*/
@funcClass(__filename)
export class SoftwareDeveloperAgent {
/**
* Runs the software developer agent to complete the user request/requirements. This will find the appropriate Git project/repository, clone it, make the changes, compile and test if applicable, commit and create a pull/merge request to review.
* @param requirements the requirements to implement. Provide ALL the details that might be required by this agent to complete the requirements task. Do not refer to details in memory etc, you must provide the actual details.
* @param scmFullProjectPath (Optional) The full path to the GitHub/GitLab etc. repository, if definitely known. (e.g. org/repo or group1/group2/project). Otherwise, leave blank, and it will be determined by searching through all the available projects.
* @returns the Merge/Pull request URL if one was created
*/
@func()
async runSoftwareDeveloperWorkflow(requirements: string, scmFullProjectPath?: string): Promise<string> {
const fileSystem = getFileSystem();
const scm = getSourceControlManagementTool();
const requirementsSummary = await this.summariseRequirements(requirements);
const gitProject = scmFullProjectPath
? (await scm.getProjects()).find((project) => project.fullPath === scmFullProjectPath)
: await this.selectProject(requirementsSummary);
logger.info(`Git project ${JSON.stringify(gitProject)}`);
const repoPath = await scm.cloneProject(gitProject.fullPath, gitProject.defaultBranch);
fileSystem.setWorkingDirectory(repoPath);
const projectInfo = await this.detectSingleProjectInfo(requirements);
// Branch setup -----------------
// TODO If we've already created the feature branch (how can we tell?) and doing more work on it, then don't need to switch to the base dev branch
// If the default branch in Gitlab/GitHub isn't the branch we want to create feature branches from, then switch to it.
let baseBranch = gitProject.defaultBranch;
if (projectInfo.devBranch && projectInfo.devBranch !== baseBranch) {
await fileSystem.vcs.switchToBranch(projectInfo.devBranch);
baseBranch = projectInfo.devBranch;
}
await fileSystem.vcs.pull();
const featureBranchName = await this.createBranchName(requirements);
await fileSystem.vcs.switchToBranch(featureBranchName);
const initialHeadSha: string = await fileSystem.vcs.getHeadSha();
try {
await new CodeEditingAgent().runCodeEditWorkflow(requirementsSummary, { projectInfo });
} catch (e) {
logger.warn(e.message);
// If no changes were made then throw an error
const currentHeadSha: string = await fileSystem.vcs.getHeadSha();
if (initialHeadSha === currentHeadSha) {
throw e;
}
// Otherwise swallow the exception so we can push the changes made so far for review
}
const { title, description } = await generatePullRequestTitleDescription(requirements, projectInfo.devBranch);
return await getSourceControlManagementTool().createMergeRequest(title, description, featureBranchName, baseBranch);
}
@cacheRetry({ scope: 'agent' })
@span()
async createBranchName(requirements: string, issueId?: string): Promise<string> {
// We always want the agent to use the same branch name when its resumed/retrying, so we cache it in the agent scope
return await createBranchName(requirements, issueId);
}
/**
* Summarises/re-writes the requirements in a clear, structured manner from the perspective of a software developer who needs is doing the implementation
* @param requirements the requirements to implement
*/
@cacheRetry()
@span()
async summariseRequirements(requirements: string): Promise<string> {
return await summariseRequirements(requirements);
}
@cacheRetry({ scope: 'agent' })
@span()
async selectProject(requirements: string): Promise<GitProject> {
return await selectProject(requirements);
}
// @cacheRetry()
async detectProjectInfo(requirements?: string): Promise<ProjectInfo[]> {
return await detectProjectInfo(requirements);
}
/**
* A projectInfo.json file may have references to sub-projects. Calling this method assumes
* there will be only one entry in the projectInfo.json file, and will throw an error if there is more
*/
async detectSingleProjectInfo(requirements?: string): Promise<ProjectInfo> {
const projectInfos = await this.detectProjectInfo(requirements);
if (projectInfos.length !== 1) throw new Error('detected project info length != 1');
const projectInfo = projectInfos[0];
logger.info(projectInfo, `Detected project info ${Object.keys(projectInfo).join(', ')}`);
return projectInfo;
}
}