diff --git a/agent/src/index.ts b/agent/src/index.ts index fb879f05f..ecd7d5113 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -48,6 +48,7 @@ import { githubInteractWithIssuePlugin, githubInteractWithPRPlugin, githubModifyIssuePlugin, + githubOrchestratePlugin, } from "@realityspiral/plugin-github"; import Database from "better-sqlite3"; import yargs from "yargs"; @@ -568,6 +569,7 @@ export async function createAgent( githubIdeationPlugin, githubInteractWithIssuePlugin, githubInteractWithPRPlugin, + githubOrchestratePlugin, ] : []), ] diff --git a/plugins/plugin-github/src/index.ts b/plugins/plugin-github/src/index.ts index 1e46d913b..ce3549956 100644 --- a/plugins/plugin-github/src/index.ts +++ b/plugins/plugin-github/src/index.ts @@ -15,6 +15,10 @@ import { createPullRequestAction, githubCreatePullRequestPlugin, } from "./plugins/createPullRequest"; +import { + forkRepositoryAction, + githubForkRepositoryPlugin, +} from "./plugins/forkRepository"; import { githubIdeationPlugin, ideationAction } from "./plugins/ideationPlugin"; import { githubInitializePlugin, @@ -29,6 +33,7 @@ import { import { addCommentToPRAction, closePRAction, + generateCodeFileChangesAction, githubInteractWithPRPlugin, implementFeatureAction, mergePRAction, @@ -39,6 +44,10 @@ import { githubModifyIssuePlugin, modifyIssueAction, } from "./plugins/modifyIssue"; +import { + githubOrchestratePlugin, + orchestrateAction, +} from "./plugins/orchestrate"; import { documentationFilesProvider } from "./providers/documentationFiles"; import { releasesProvider } from "./providers/releases"; import { sourceCodeProvider } from "./providers/sourceCode"; @@ -55,6 +64,8 @@ export const plugins = { githubInteractWithIssuePlugin, githubInteractWithPRPlugin, githubIdeationPlugin, + githubOrchestratePlugin, + githubForkRepositoryPlugin, }; export * from "./plugins/initializeRepository"; @@ -66,6 +77,7 @@ export * from "./plugins/modifyIssue"; export * from "./plugins/interactWithIssue"; export * from "./plugins/ideationPlugin"; export * from "./plugins/interactWithPR"; +export * from "./plugins/orchestrate"; export * from "./providers/sourceCode"; export * from "./providers/testFiles"; @@ -98,7 +110,10 @@ export const githubPlugin: Plugin = { reactToIssueAction, closeIssueAction, replyToPRCommentAction, + generateCodeFileChangesAction, implementFeatureAction, + forkRepositoryAction, + orchestrateAction, ], evaluators: [], providers: [ diff --git a/plugins/plugin-github/src/plugins/createCommit.ts b/plugins/plugin-github/src/plugins/createCommit.ts index ce5052f29..745477a78 100644 --- a/plugins/plugin-github/src/plugins/createCommit.ts +++ b/plugins/plugin-github/src/plugins/createCommit.ts @@ -90,9 +90,12 @@ export const createCommitAction: Action = { const repoPath = getRepoPath(content.owner, content.repo); try { + const token = runtime.getSetting("GITHUB_API_TOKEN"); + await checkoutBranch(repoPath, content.branch, true); await writeFiles(repoPath, content.files); const commit = await commitAndPushChanges( + token, repoPath, content.message, content.branch, diff --git a/plugins/plugin-github/src/plugins/createIssue.ts b/plugins/plugin-github/src/plugins/createIssue.ts index 871358f8c..cf1372d13 100644 --- a/plugins/plugin-github/src/plugins/createIssue.ts +++ b/plugins/plugin-github/src/plugins/createIssue.ts @@ -22,6 +22,7 @@ import { type SimilarityIssueCheckContent, SimilarityIssueCheckSchema, isCreateIssueContent, + isSimilarityIssueCheckContent, } from "../types"; import { saveIssueToMemory } from "../utils"; export const createIssueAction: Action = { @@ -119,8 +120,8 @@ export const createIssueAction: Action = { schema: SimilarityIssueCheckSchema, }); - if (!isCreateIssueContent(details.object)) { - elizaLogger.error("Invalid content:", details.object); + if (!isSimilarityIssueCheckContent(similarityCheckDetails.object)) { + elizaLogger.error("Invalid content:", similarityCheckDetails.object); throw new Error("Invalid content"); } diff --git a/plugins/plugin-github/src/plugins/createMemoriesFromFiles.ts b/plugins/plugin-github/src/plugins/createMemoriesFromFiles.ts index a73e660b0..d4984fde9 100644 --- a/plugins/plugin-github/src/plugins/createMemoriesFromFiles.ts +++ b/plugins/plugin-github/src/plugins/createMemoriesFromFiles.ts @@ -126,6 +126,9 @@ export const createMemoriesFromFilesAction: Action = { template: createMemoriesFromFilesTemplate, }); + // write context to a file + await fs.writeFile("/tmp/create-memories-from-files-context.txt", context); + const details = await generateObject({ runtime, context, @@ -133,6 +136,12 @@ export const createMemoriesFromFilesAction: Action = { schema: CreateMemoriesFromFilesSchema, }); + // write details to a file + await fs.writeFile( + "/tmp/create-memories-from-files-details.txt", + JSON.stringify(details, null, 2), + ); + if (!isCreateMemoriesFromFilesContent(details.object)) { throw new Error("Invalid content"); } diff --git a/plugins/plugin-github/src/plugins/createPullRequest.ts b/plugins/plugin-github/src/plugins/createPullRequest.ts index f8697b2c3..eb31a89fe 100644 --- a/plugins/plugin-github/src/plugins/createPullRequest.ts +++ b/plugins/plugin-github/src/plugins/createPullRequest.ts @@ -89,11 +89,18 @@ export const createPullRequestAction: Action = { const repoPath = getRepoPath(content.owner, content.repo); try { + const token = runtime.getSetting("GITHUB_API_TOKEN"); + await checkoutBranch(repoPath, content.branch, true); await writeFiles(repoPath, content.files); - await commitAndPushChanges(repoPath, content.title, content.branch); + await commitAndPushChanges( + token, + repoPath, + content.title, + content.branch, + ); const pullRequest = await createPullRequest( - runtime.getSetting("GITHUB_API_TOKEN"), + token, content.owner, content.repo, content.branch, @@ -108,7 +115,7 @@ export const createPullRequestAction: Action = { content.owner, content.repo, content.branch, - runtime.getSetting("GITHUB_API_TOKEN"), + token, ); elizaLogger.info( diff --git a/plugins/plugin-github/src/plugins/forkRepository.ts b/plugins/plugin-github/src/plugins/forkRepository.ts new file mode 100644 index 000000000..1cad43b99 --- /dev/null +++ b/plugins/plugin-github/src/plugins/forkRepository.ts @@ -0,0 +1,154 @@ +import { + type Action, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type Plugin, + type State, + composeContext, + elizaLogger, + generateObject, +} from "@elizaos/core"; +import { GitHubService } from "../services/github"; +import { forkRepositoryTemplate } from "../templates"; +import { + type ForkRepositoryContent, + ForkRepositorySchema, + isForkRepositoryContent, +} from "../types"; + +export const forkRepositoryAction: Action = { + name: "FORK_REPOSITORY", + similes: [ + "FORK_REPO", + "FORK", + "GITHUB_FORK", + "GITHUB_FORK_REPO", + "GITHUB_FORK_REPOSITORY", + "CREATE_FORK", + ], + description: "Forks a GitHub repository", + validate: async (runtime: IAgentRuntime) => { + const token = !!runtime.getSetting("GITHUB_API_TOKEN"); + return token; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State, + // biome-ignore lint/suspicious/noExplicitAny: + _options?: any, + callback?: HandlerCallback, + ) => { + if (!state) { + // biome-ignore lint/style/noParameterAssign: + state = (await runtime.composeState(message)) as State; + } else { + // biome-ignore lint/style/noParameterAssign: + state = await runtime.updateRecentMessageState(state); + } + + const context = composeContext({ + state, + template: forkRepositoryTemplate, + }); + + const details = await generateObject({ + runtime, + context, + modelClass: ModelClass.SMALL, + schema: ForkRepositorySchema, + }); + + if (!isForkRepositoryContent(details.object)) { + elizaLogger.error("Invalid content:", details.object); + throw new Error("Invalid content"); + } + + const content = details.object as ForkRepositoryContent; + + elizaLogger.info(`Forking repository ${content.owner}/${content.repo}...`); + + const githubService = new GitHubService({ + owner: content.owner, + repo: content.repo, + auth: runtime.getSetting("GITHUB_API_TOKEN"), + }); + + try { + const fork = await githubService.forkRepository( + content.owner, + content.repo, + content.organization, + ); + + elizaLogger.info(`Repository forked successfully! URL: ${fork.html_url}`); + + if (callback) { + callback({ + text: `Repository forked successfully! URL: ${fork.html_url}`, + action: "FORK_REPOSITORY", + source: "github", + attachments: [], + }); + } + + return fork; + } catch (error) { + elizaLogger.error( + `Error forking repository ${content.owner}/${content.repo}:`, + error, + ); + + if (callback) { + callback( + { + text: `Error forking repository ${content.owner}/${content.repo}. Please try again.`, + action: "FORK_REPOSITORY", + source: "github", + }, + [], + ); + } + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Fork repository octocat/Hello-World", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Repository forked successfully! URL: https://github.com/user1/Hello-World", + action: "FORK_REPOSITORY", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Create a fork of repository octocat/Hello-World to my-org organization", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Repository forked successfully! URL: https://github.com/my-org/Hello-World", + action: "FORK_REPOSITORY", + }, + }, + ], + ], +}; + +export const githubForkRepositoryPlugin: Plugin = { + name: "githubForkRepository", + description: "Integration with GitHub for forking repositories", + actions: [forkRepositoryAction], +}; diff --git a/plugins/plugin-github/src/plugins/initializeRepository.ts b/plugins/plugin-github/src/plugins/initializeRepository.ts index 530e4eb69..3c7e206b3 100644 --- a/plugins/plugin-github/src/plugins/initializeRepository.ts +++ b/plugins/plugin-github/src/plugins/initializeRepository.ts @@ -21,6 +21,7 @@ import { cloneOrPullRepository, createReposDirectory, getRepoPath, + initRepo, } from "../utils"; export const initializeRepositoryAction: Action = { @@ -87,25 +88,13 @@ export const initializeRepositoryAction: Action = { `Initializing repository ${content.owner}/${content.repo} on branch ${content.branch}...`, ); - const repoPath = getRepoPath(content.owner, content.repo); - - elizaLogger.info(`Repository path: ${repoPath}`); - try { const token = runtime.getSetting("GITHUB_API_TOKEN"); if (!token) { throw new Error("GITHUB_API_TOKEN is not set"); } - await createReposDirectory(content.owner); - await cloneOrPullRepository( - token, - content.owner, - content.repo, - repoPath, - content.branch, - ); - await checkoutBranch(repoPath, content.branch); + await initRepo(token, content.owner, content.repo, content.branch); elizaLogger.info( `Repository initialized successfully! URL: https://github.com/${content.owner}/${content.repo} @ branch: ${content.branch}`, diff --git a/plugins/plugin-github/src/plugins/interactWithPR.ts b/plugins/plugin-github/src/plugins/interactWithPR.ts index 7c2a4c1ea..cf1adfab8 100644 --- a/plugins/plugin-github/src/plugins/interactWithPR.ts +++ b/plugins/plugin-github/src/plugins/interactWithPR.ts @@ -1,3 +1,4 @@ +import fs from "node:fs/promises"; import { type Action, Content, @@ -989,6 +990,88 @@ export const replyToPRCommentAction: Action = { ], }; +export const generateCodeFileChangesAction: Action = { + name: "GENERATE_CODE_FILE_CHANGES", + similes: [ + "GENERATE_CODE_FILE_CHANGES", + "GENERATE_CODE_CHANGES", + "GENERATE_FILE_CHANGES", + ], + description: + "Generates code file changes based on feature requirements or issue details", + validate: async (_runtime: IAgentRuntime) => { + return true; // No specific validation needed + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State, + // biome-ignore lint/suspicious/noExplicitAny: + _options?: any, + callback?: HandlerCallback, + ) => { + if (!state) { + // biome-ignore lint/style/noParameterAssign: + state = (await runtime.composeState(message)) as State; + } else { + // biome-ignore lint/style/noParameterAssign: + state = await runtime.updateRecentMessageState(state); + } + + const context = composeContext({ + state, + template: generateCodeFileChangesTemplate, + }); + + const details = await generateObject({ + runtime, + context, + modelClass: ModelClass.SMALL, + schema: GenerateCodeFileChangesSchema, + }); + + if (!isGenerateCodeFileChangesContent(details.object)) { + elizaLogger.error("Invalid code file changes content:", details.object); + throw new Error("Invalid code file changes content"); + } + + const content = details.object as GenerateCodeFileChangesContent; + + // write file + await fs.writeFile( + "/tmp/generated-code-file-changes.json", + JSON.stringify(content, null, 2), + ); + + if (callback) { + await callback({ + text: "Generated code file changes successfully!", + action: "GENERATE_CODE_FILE_CHANGES", + attachments: [], + }); + } + + return content; + }, + examples: [ + [ + { + user: "{{user}}", + content: { + text: "Generate code changes for replacing console.log with elizaLogger.log", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Generated code file changes successfully!", + action: "GENERATE_CODE_FILE_CHANGES", + }, + }, + ], + ], +}; + export const implementFeatureAction: Action = { name: "IMPLEMENT_FEATURE", similes: ["IMPLEMENT_FEATURE", "REPLACE_LOGS"], @@ -1068,33 +1151,22 @@ export const implementFeatureAction: Action = { } state.specificIssue = JSON.stringify(issue, null, 2); - // Generate code file changes - const codeFileChangesContext = composeContext({ - state, - template: generateCodeFileChangesTemplate, - }); - - const codeFileChangesDetails = await generateObject({ - runtime, - context: codeFileChangesContext, - modelClass: ModelClass.SMALL, - schema: GenerateCodeFileChangesSchema, - }); - - if (!isGenerateCodeFileChangesContent(codeFileChangesDetails.object)) { - elizaLogger.error( - "Invalid code file changes content:", - codeFileChangesDetails.object, - ); - throw new Error("Invalid code file changes content"); - } + // Use generateCodeFileChangesAction + message.content.text = `Generate code changes for ${content.feature}`; const codeFileChangesContent = - codeFileChangesDetails.object as GenerateCodeFileChangesContent; + (await generateCodeFileChangesAction.handler( + runtime, + message, + state, + options, + )) as GenerateCodeFileChangesContent; + state.codeFileChanges = codeFileChangesContent.files; - elizaLogger.info( - "Generated code file changes successfully!", + // write file + await fs.writeFile( + "/tmp/implement-feature-code-file-changes.json", JSON.stringify(codeFileChangesContent, null, 2), ); @@ -1204,6 +1276,7 @@ export const githubInteractWithPRPlugin: Plugin = { closePRAction, mergePRAction, replyToPRCommentAction, - implementFeatureAction, + generateCodeFileChangesAction, + // implementFeatureAction, ], }; diff --git a/plugins/plugin-github/src/plugins/orchestrate.ts b/plugins/plugin-github/src/plugins/orchestrate.ts new file mode 100644 index 000000000..86ebef5d4 --- /dev/null +++ b/plugins/plugin-github/src/plugins/orchestrate.ts @@ -0,0 +1,195 @@ +import fs from "node:fs/promises"; +import { + type Action, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type Plugin, + type State, + composeContext, + elizaLogger, + generateObject, +} from "@elizaos/core"; +import { + type OrchestratedGithubAction, + OrchestrationSchema, + isOrchestrationSchema, + orchestrationTemplate, + plugins, +} from "../index"; + +export const orchestrateAction: Action = { + name: "ORCHESTRATE", + similes: [ + "ORCHESTRATE", + "PLAN", + "SEQUENCE", + "EXECUTE_PLAN", + "ORCHESTRATE_ACTIONS", + ], + description: + "Orchestrates a sequence of actions to fulfill a complex request", + validate: async (runtime: IAgentRuntime) => { + const token = !!runtime.getSetting("GITHUB_API_TOKEN"); + return token; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State, + // biome-ignore lint/suspicious/noExplicitAny: + _options?: any, + callback?: HandlerCallback, + ) => { + if (!state) { + // biome-ignore lint/style/noParameterAssign: + state = await runtime.composeState(message); + } else { + // biome-ignore lint/style/noParameterAssign: + state = await runtime.updateRecentMessageState(state); + } + + // Get the orchestration plan + const plan = await generateOrchestrationPlan(runtime, message, state); + + for (const action of plan) { + try { + const result = await executeAction(runtime, action, state, callback); + + // write result to a file + await fs.writeFile( + `/tmp/orchestrate-result-${action.githubAction}.json`, + JSON.stringify(result, null, 2), + ); + } catch (error) { + elizaLogger.error( + `Error executing action ${action.githubAction}:`, + error, + ); + if (callback) { + callback({ + text: `Error executing action ${action.githubAction}. Please try again.`, + action: "ORCHESTRATE", + source: "github", + }); + } + throw error; + } + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Orchestrate a new feature to improve user experience in user1/repo1", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Successfully executed orchestration plan for implementing new feature", + action: "ORCHESTRATE", + }, + }, + ], + ], +}; + +async function generateOrchestrationPlan( + runtime: IAgentRuntime, + _message: Memory, + state?: State, +): Promise { + const context = composeContext({ + state, + template: orchestrationTemplate, + }); + + // write the context to a file + await fs.writeFile("/tmp/orchestration-context.txt", context); + + const details = await generateObject({ + runtime, + context, + modelClass: ModelClass.SMALL, + schema: OrchestrationSchema, + }); + + if (!isOrchestrationSchema(details.object)) { + elizaLogger.error("Invalid content:", details.object); + throw new Error("Invalid content"); + } + + const content = details.object as OrchestrationSchema; + + // write the details to a file + await fs.writeFile( + "/tmp/orchestration-details.txt", + JSON.stringify(content, null, 2), + ); + + return content.githubActions; +} + +async function executeAction( + runtime: IAgentRuntime, + orchestratedAction: OrchestratedGithubAction, + state?: State, + callback?: HandlerCallback, + // biome-ignore lint/suspicious/noExplicitAny: +): Promise { + // Find the action handler + const actionHandler = findActionHandler(orchestratedAction.githubAction); + if (!actionHandler) { + throw new Error(`Action ${orchestratedAction.githubAction} not found`); + } + + // Create a new memory for this action + const actionMemory: Memory = { + content: { + text: orchestratedAction.user, + action: orchestratedAction.githubAction, + source: "github", + }, + userId: state.userId, + agentId: state.agentId, + roomId: state.roomId, + }; + + // create memory for the action + await runtime.messageManager.createMemory(actionMemory); + + // Execute the action + const result = + (await actionHandler.handler( + runtime, + actionMemory, + null, + undefined, + callback, + )) || {}; + + return result; +} + +function findActionHandler(actionName: string): Action | undefined { + // Search through all plugins for the action + for (const plugin of Object.values(plugins)) { + const actions = plugin.actions || []; + const action = actions.find( + (a) => a.name === actionName || a.similes?.includes(actionName), + ); + if (action) { + return action; + } + } + return undefined; +} + +export const githubOrchestratePlugin: Plugin = { + name: "githubOrchestrate", + description: "Integration with GitHub for orchestrating complex operations", + actions: [orchestrateAction], +}; diff --git a/plugins/plugin-github/src/services/github.ts b/plugins/plugin-github/src/services/github.ts index e1cdbb10f..c6a2e4771 100644 --- a/plugins/plugin-github/src/services/github.ts +++ b/plugins/plugin-github/src/services/github.ts @@ -1017,13 +1017,13 @@ export class GitHubService { repo, branch, ); - console.log( + elizaLogger.log( `Latest commit SHA on branch '${branch}': ${latestCommitSha}`, ); // Step 2: Get the tree SHA from the latest commit const baseTreeSha = await this.getTreeSha(owner, repo, latestCommitSha); - console.log(`Base tree SHA: ${baseTreeSha}`); + elizaLogger.log(`Base tree SHA: ${baseTreeSha}`); // Step 3: Create a new tree with the file changes const newTreeSha = await this.createNewTree( @@ -1032,7 +1032,7 @@ export class GitHubService { baseTreeSha, files, ); - console.log(`New tree SHA: ${newTreeSha}`); + elizaLogger.log(`New tree SHA: ${newTreeSha}`); // Step 4: Create a new commit const { data: newCommit } = await this.octokit.git.createCommit({ @@ -1046,7 +1046,7 @@ export class GitHubService { return newCommit; } catch (error) { - console.error("Error creating commit:", error); + elizaLogger.error("Error creating commit:", error); throw error; } } @@ -1072,12 +1072,29 @@ export class GitHubService { sha: newCommitSha, force: false, // Set to true if you need to force update }); - console.log(`Branch '${branch}' updated to commit SHA: ${newCommitSha}`); + elizaLogger.log( + `Branch '${branch}' updated to commit SHA: ${newCommitSha}`, + ); } catch (error) { console.error("Error updating branch reference:", error); throw error; } } + + async forkRepository(owner: string, repo: string, organization?: string) { + try { + const response = await this.octokit.repos.createFork({ + owner: owner, + repo: repo, + organization: organization, + }); + elizaLogger.log(`Fork created successfully @ ${response.data.html_url}`); + return response.data; + } catch (error) { + elizaLogger.error("Error creating fork:", error); + throw error; + } + } } export type { GitHubConfig }; diff --git a/plugins/plugin-github/src/templates.ts b/plugins/plugin-github/src/templates.ts index c88a393d0..789daafbf 100644 --- a/plugins/plugin-github/src/templates.ts +++ b/plugins/plugin-github/src/templates.ts @@ -1658,3 +1658,365 @@ Examples: \`\`\` `; + +export const orchestrationTemplate = ` +**You are a github action orchestrator.** Your role is to analyze the user message and determine the sequence of github actions required to fulfill the request. Both the user message and system message in each github action must be explicitly written in the context of the user's request, ensuring that it aligns precisely with their intent. The github actions must be selected from the predefined list provided in JSON format and structured as JSON arrays. +**Important Instructions:** +1. The following actions are mandatory and must be executed at the beginning of every action sequence in this order: + - INITIALIZE_REPOSITORY + - CREATE_MEMORIES_FROM_FILES +2. Only return the list of github actions in JSON format, without any explanations or additional content. +3. Ensure that both the user message and system message in each github action accurately incorporate the specific details from the user's request. +4. Ensure the github actions are logically ordered to achieve the desired outcome efficiently. +5. The output must adhere strictly to the format and examples provided below. +--- +Here are the recent user messages for context: +{{recentMessages}} +--- +### **Available Github Actions** +Here is the complete list of github actions you can use, where each github action shows an example user message followed by the system's response: +\`\`\`json +{ + "CREATE_MEMORIES_FROM_FILES": { + "user": "Create memories from files on repository octocat/hello-world @ branch main and path '/'", + "system": "Memories created successfully!" + }, + "INITIALIZE_REPOSITORY": { + "user": "Initialize the repository user1/repo1 on main branch", + "system": "Repository initialized successfully! URL: https://github.com/user1/repo1" + }, +} +\`\`\` +--- +### **Examples** +**Example 1: Implement a new feature** +User: "Plan and implement a new feature to improve user experience in user1/repo1." +\`\`\`json +[ + { + "githubAction": "INITIALIZE_REPOSITORY", + "user": "Initialize the repository user1/repo1 on main branch", + "system": "Repository initialized successfully! URL: https://github.com/user1/repo1" + }, + { + "githubAction": "CREATE_MEMORIES_FROM_FILES", + "user": "Create memories from files on repository user1/repo1 @ branch main and path '/'", + "system": "Memories created successfully!" + } +] +\`\`\` +**Example 2: Bug resolution** +User: "There's a critical bug in the login system of user1/repo1 that needs to be fixed immediately." +\`\`\`json +[ + { + "githubAction": "INITIALIZE_REPOSITORY", + "user": "Initialize the repository user1/repo1 on main branch", + "system": "Repository initialized successfully! URL: https://github.com/user1/repo1" + }, + { + "githubAction": "CREATE_MEMORIES_FROM_FILES", + "user": "Create memories from files on repository user1/repo1 @ branch main and path '/'", + "system": "Memories created successfully!" + } +] +\`\`\` +**Example 3: Documentation update** +User: "Update the architecture documentation in octocat/hello-world." +\`\`\`json +[ + { + "githubAction": "INITIALIZE_REPOSITORY", + "user": "Initialize the repository octocat/hello-world on main branch", + "system": "Repository initialized successfully! URL: https://github.com/octocat/hello-world" + }, + { + "githubAction": "CREATE_MEMORIES_FROM_FILES", + "user": "Create memories from files on repository octocat/hello-world @ branch main and path '/'", + "system": "Memories created successfully!" + } +] +\`\`\` +`; + +export const orchestrationTemplateOriginal = ` +**You are a github action orchestrator.** Your role is to analyze the user message and determine the sequence of github actions required to fulfill the request. Both the user message and system message in each github action must be explicitly written in the context of the user's request, ensuring that it aligns precisely with their intent. The github actions must be selected from the predefined list provided in JSON format and structured as JSON arrays. +**Important Instructions:** +1. The following actions are mandatory and must be executed at the beginning of every action sequence in this order: + - INITIALIZE_REPOSITORY + - CREATE_MEMORIES_FROM_FILES +2. Only return the list of github actions in JSON format, without any explanations or additional content. +3. Ensure that both the user message and system message in each github action accurately incorporate the specific details from the user's request. +4. Ensure the github actions are logically ordered to achieve the desired outcome efficiently. +5. The output must adhere strictly to the format and examples provided below. +--- +Here are the recent user messages for context: +{{recentMessages}} +--- +### **Available Github Actions** +Here is the complete list of github actions you can use, where each github action shows an example user message followed by the system's response: +\`\`\`json +{ + "CREATE_COMMIT": { + "user": "Create a commit in the repository user1/repo1 on branch 'main' with the commit message: 'Fix bug'", + "system": "Changes commited to repository user1/repo1 successfully to branch 'main'! commit hash: " + }, + "CREATE_ISSUE": { + "user": "Create an issue in repository user1/repo1 titled 'Bug: Application crashes on startup'", + "system": "Created issue # successfully!" + }, + "CREATE_MEMORIES_FROM_FILES": { + "user": "Create memories from files on repository octocat/hello-world @ branch main and path '/'", + "system": "Memories created successfully!" + }, + "CREATE_PULL_REQUEST": { + "user": "Create a pull request on repository octocat/hello-world with branch 'fix/something' against base 'develop', title 'fix: something' and files 'docs/architecture.md'", + "system": "Pull request created successfully! URL: https://github.com/octocat/hello-world/pull/ @ branch: 'fix/something'" + }, + "INITIALIZE_REPOSITORY": { + "user": "Initialize the repository user1/repo1 on main branch", + "system": "Repository initialized successfully! URL: https://github.com/user1/repo1" + }, + "COMMENT_ON_ISSUE": { + "user": "Add a comment to issue #1 in repository user1/repo1: 'This is fixed in the latest release'", + "system": "Added comment to issue #1 successfully! See comment at https://github.com/user1/repo1/issues/1#issuecomment-" + }, + "REACT_TO_ISSUE": { + "user": "React to issue #1 in repository user1/repo1 with a heart", + "system": "Added reaction to issue #1 successfully!" + }, + "CLOSE_ISSUE": { + "user": "Close issue #1 in repository user1/repo1", + "system": "Closed issue #1 successfully!" + }, + "REACT_TO_PULL_REQUEST": { + "user": "React to pull request #1 in repository user1/repo1 with a thumbs up", + "system": "Added reaction to pull request #1 successfully!" + }, + "ADD_COMMENT_PR": { + "user": "Add a comment to pull request #1 in repository user1/repo1: 'This is fixed in the latest release'", + "system": "Added comment to pull request #1 successfully!" + }, + "CLOSE_PR": { + "user": "Close pull request #1 in repository user1/repo1", + "system": "Closed pull request #1 successfully!" + }, + "MERGE_PULL_REQUEST": { + "user": "Merge pull request #1 in repository user1/repo1 using merge method 'squash'", + "system": "Merged pull request #1 successfully!" + }, + "REPLY_TO_ALL_PR_COMMENTS": { + "user": "Reply to all comments in pull request #1 in repository user1/repo1", + "system": "Replied to all comments in pull request #1 successfully!" + }, + "MODIFY_ISSUE": { + "user": "Update issue #1 in repository user1/repo1 to add the label 'bug'", + "system": "Modified issue #1 successfully!" + }, + "IDEATION": { + "user": "Ideate on improving the frontend of user2/repo2", + "system": "Analyzing the repository and considering previous user feedback, here are some frontend improvement ideas:\n\n1. **Redesign UI for Better UX**: The current design could be more intuitive. Implementing a modern design framework like Material-UI could enhance user experience.\n2. **Improve Accessibility**: Past user feedback has pointed out accessibility issues. Implementing ARIA roles and ensuring color contrast could be beneficial. Consider using tools like Lighthouse to audit accessibility.\n3. **Optimize Asset Loading**: There are opportunities to lazy-load images and scripts to improve page load times, as noted in previous performance audits. This could significantly enhance the user experience on slower networks.\n4. **Enhance State Management**: Transitioning to a more robust state management solution like Redux could address issues raised in past bug reports, particularly those related to data consistency.\n5. **Implement Responsive Design**: Ensuring the application is fully responsive was a common request in user surveys. Use CSS Grid and Flexbox to create a fluid layout that adapts to different screen sizes." + }, + "GENERATE_CODE_FILE_CHANGES": { + "user": "Generate code file changes for improving the user experience in repository user1/repo1", + "system": "Here are the list of code file changes to be made to improve the user experience in repository user1/repo1:\n\n1. ****:\n\`\`\`\n\`\`\`\n2. ****:\n\`\`\`\n\`\`\`\n3. ****:\n\`\`\`\n\`\`\`\n4. etc." + }, + "FORK_REPOSITORY": { + "user": "Fork repository user1/repo1", + "system": "Forked repository user1/repo1 successfully! Fork URL: https://github.com//repo1" + } +} +\`\`\` +--- +### **Examples** +**Example 1: Implement a new feature** +User: "Plan and implement a new feature to improve user experience in user1/repo1." +\`\`\`json +[ + { + "githubAction": "INITIALIZE_REPOSITORY", + "user": "Initialize the repository user1/repo1 on main branch", + "system": "Repository initialized successfully! URL: https://github.com/user1/repo1" + }, + { + "githubAction": "CREATE_MEMORIES_FROM_FILES", + "user": "Create memories from files on repository user1/repo1 @ branch main and path '/'", + "system": "Memories created successfully!" + }, + { + "githubAction": "IDEATION", + "user": "Ideate on improving the frontend of user1/repo1", + "system": "Analyzing the repository and considering previous user feedback, here are some frontend improvement ideas:\n\n1. ****: \n2. ****: \n3. ****: \n4. etc." + }, + { + "githubAction": "CREATE_ISSUE", + "user": "Create an issue for improving the user experience in repository user1/repo1 as a user story and list out all the steps that need to be taken to implement the feature", + "system": "Created issue # successfully!" + }, + { + "githubAction": "GENERATE_CODE_FILE_CHANGES", + "user": "Generate code file changes for improving the user experience in repository user1/repo1", + "system": "Here are the list of code file changes to be made to improve the user experience in repository user1/repo1:\n\n1. ****:\n\`\`\`\n\`\`\`\n2. ****:\n\`\`\`\n\`\`\`\n3. ****:\n\`\`\`\n\`\`\`\n4. etc." + }, + { + "githubAction": "CREATE_COMMIT", + "user": "Create a commit in the repository user1/repo1 on branch 'feature/improve-ux' with the commit message: 'Improve user experience'", + "system": "Changes commited to repository user1/repo1 successfully to branch 'feature/improve-ux'! commit hash: " + }, + { + "githubAction": "FORK_REPOSITORY", + "user": "Fork repository user1/repo1", + "system": "Forked repository user1/repo1 successfully! Fork URL: https://github.com//repo1" + }, + { + "githubAction": "CREATE_PULL_REQUEST", + "user": "Create a pull request on repository /repo1 with branch 'feature/improve-ux', title 'Improve user experience' against base 'main'", + "system": "Pull request created successfully! URL: https://github.com/user1/repo1/pull/ @ branch: 'feature/improve-ux'", + }, + { + "githubAction": "MERGE_PULL_REQUEST", + "user": "Merge pull request #1 in repository user1/repo1 using merge method 'squash'", + "system": "Merged pull request #1 successfully!" + }, + { + "githubAction": "CLOSE_ISSUE", + "user": "Close issue #1 in repository user1/repo1", + "system": "Closed issue #1 successfully!" + } +] +\`\`\` +**Example 2: Bug resolution** +User: "There's a critical bug in the login system of user1/repo1 that needs to be fixed immediately." +\`\`\`json +[ + { + "githubAction": "INITIALIZE_REPOSITORY", + "user": "Initialize the repository user1/repo1 on main branch", + "system": "Repository initialized successfully! URL: https://github.com/user1/repo1" + }, + { + "githubAction": "CREATE_MEMORIES_FROM_FILES", + "user": "Create memories from files on repository user1/repo1 @ branch main and path '/'", + "system": "Memories created successfully!" + }, + { + "githubAction": "CREATE_ISSUE", + "user": "Create an issue in repository user1/repo1 titled 'Critical Bug: Login System Failure'", + "system": "Created issue # successfully!" + }, + { + "githubAction": "MODIFY_ISSUE", + "user": "Update issue #1 in repository user1/repo1 to add the label 'bug'", + "system": "Modified issue #1 successfully!" + }, + { + "githubAction": "GENERATE_CODE_FILE_CHANGES", + "user": "Generate code file changes to fix the login system in repository user1/repo1", + "system": "Here are the list of code file changes to be made to fix the login system in repository user1/repo1:\n\n1. ****: \`\`\`\n\`\`\`\n2. ****: \`\`\`\n\`\`\`\n3. ****: \`\`\`\n\`\`\`\n4. etc." + }, + { + "githubAction": "CREATE_COMMIT", + "user": "Create a commit in the repository user1/repo1 on branch 'fix/login-system' with the commit message: 'Fix: Login system critical bug'", + "system": "Changes commited to repository user1/repo1 successfully to branch 'fix/login-system'! commit hash: " + }, + { + "githubAction": "FORK_REPOSITORY", + "user": "Fork repository user1/repo1", + "system": "Forked repository user1/repo1 successfully! Fork URL: https://github.com//repo1" + }, + { + "githubAction": "CREATE_PULL_REQUEST", + "user": "Create a pull request on repository /repo1 with branch 'fix/login-system', title 'Fix: Login System Critical Bug' against base 'main'", + "system": "Pull request created successfully! URL: https://github.com/user1/repo1/pull/ @ branch: 'fix/login-system'" + }, + { + "githubAction": "MERGE_PULL_REQUEST", + "user": "Merge pull request #1 in repository user1/repo1 using merge method 'squash'", + "system": "Merged pull request #1 successfully!" + }, + { + "githubAction": "CLOSE_ISSUE", + "user": "Close issue #1 in repository user1/repo1", + "system": "Closed issue #1 successfully!" + } +] +\`\`\` +**Example 3: Documentation update** +User: "Update the architecture documentation in octocat/hello-world." +\`\`\`json +[ + { + "githubAction": "INITIALIZE_REPOSITORY", + "user": "Initialize the repository octocat/hello-world on main branch", + "system": "Repository initialized successfully! URL: https://github.com/octocat/hello-world" + }, + { + "githubAction": "CREATE_MEMORIES_FROM_FILES", + "user": "Create memories from files on repository octocat/hello-world @ branch main and path '/'", + "system": "Memories created successfully!" + }, + { + "githubAction": "IDEATION", + "user": "Ideate on improving the architecture of octocat/hello-world", + "system": "Analyzing the repository and considering previous user feedback, here are some architecture improvement ideas:\n\n1. ****: \n2. ****: \n3. ****: \n4. etc." + }, + { + "githubAction": "CREATE_ISSUE", + "user": "Create an issue in repository octocat/hello-world titled 'Update: Architecture Documentation'", + "system": "Created issue # successfully!" + }, + { + "githubAction": "GENERATE_CODE_FILE_CHANGES", + "user": "Generate code file changes to update the architecture documentation in repository octocat/hello-world", + "system": "Here are the list of code file changes to be made to update the architecture documentation in repository octocat/hello-world:\n\n1. ****: \`\`\`\n\`\`\`\n2. ****: \`\`\`\n\`\`\`\n3. ****: \`\`\`\n\`\`\`\n4. etc." + }, + { + "githubAction": "CREATE_COMMIT", + "user": "Create a commit in the repository octocat/hello-world on branch 'docs/architecture-update' with the commit message: 'docs: Update Architecture Documentation'", + "system": "Changes commited to repository octocat/hello-world successfully to branch 'docs/architecture-update'! commit hash: " + }, + { + "githubAction": "FORK_REPOSITORY", + "user": "Fork repository octocat/hello-world", + "system": "Forked repository octocat/hello-world successfully! Fork URL: https://github.com//hello-world" + }, + { + "githubAction": "CREATE_PULL_REQUEST", + "user": "Create a pull request on repository /hello-world with branch 'docs/architecture-update' against base 'main', title 'docs: Update Architecture Documentation' and files 'docs/architecture.md'", + "system": "Pull request created successfully! URL: https://github.com/octocat/hello-world/pull/ @ branch: 'docs/architecture-update'" + }, + { + "githubAction": "REACT_TO_PULL_REQUEST", + "user": "React to pull request #1 in repository octocat/hello-world with a thumbs up", + "system": "Added reaction to pull request #1 successfully!" + }, + { + "githubAction": "MERGE_PULL_REQUEST", + "user": "Merge pull request #1 in repository octocat/hello-world using merge method 'squash'", + "system": "Merged pull request #1 successfully!" + }, + { + "githubAction": "CLOSE_ISSUE", + "user": "Close issue #1 in repository octocat/hello-world", + "system": "Closed issue #1 successfully!" + } +] +\`\`\` +`; + +export const forkRepositoryTemplate = ` +Based on the user's message, extract the repository owner and name, and optionally the target organization for forking. +If the organization is not specified, the repository will be forked to the authenticated user's account. + +Example user messages: +- "Fork repository octocat/Hello-World" +- "Create a fork of repository octocat/Hello-World to my-org organization" +- "Fork octocat/Hello-World to my-organization" + +The response should include: +- owner: The owner of the repository to fork +- repo: The name of the repository to fork +- organization: (Optional) The organization to fork the repository to + +User message: {{message.content.text}} +`; diff --git a/plugins/plugin-github/src/types.ts b/plugins/plugin-github/src/types.ts index cf827ef4d..18819654c 100644 --- a/plugins/plugin-github/src/types.ts +++ b/plugins/plugin-github/src/types.ts @@ -586,3 +586,58 @@ export const isGenerateCodeFileChangesContent = ( elizaLogger.error("Invalid content: ", object); return false; }; + +// Schema for a single orchestrated action +export const OrchestratedGithubActionSchema = z.object({ + githubAction: z.string(), + user: z.string(), + system: z.string(), +}); + +// Type for a single orchestrated action +export interface OrchestratedGithubAction { + githubAction: string; + user: string; + system: string; +} + +// Schema for the orchestration response +export const OrchestrationSchema = z.object({ + githubActions: z.array(OrchestratedGithubActionSchema), +}); + +export interface OrchestrationSchema { + githubActions: OrchestratedGithubAction[]; +} + +export const isOrchestrationSchema = ( + // biome-ignore lint/suspicious/noExplicitAny: + object: any, +): object is OrchestrationSchema => { + return OrchestrationSchema.safeParse(object).success; +}; + +export interface ForkRepositoryContent { + owner: string; + repo: string; + organization?: string; +} + +export const ForkRepositorySchema = z.object({ + owner: z.string(), + repo: z.string(), + organization: z.string().optional(), +}); + +export function isForkRepositoryContent( + obj: unknown, +): obj is ForkRepositoryContent { + if (!obj || typeof obj !== "object") return false; + const content = obj as ForkRepositoryContent; + return ( + typeof content.owner === "string" && + typeof content.repo === "string" && + (content.organization === undefined || + typeof content.organization === "string") + ); +} diff --git a/plugins/plugin-github/src/utils.ts b/plugins/plugin-github/src/utils.ts index def29387b..653c770a2 100644 --- a/plugins/plugin-github/src/utils.ts +++ b/plugins/plugin-github/src/utils.ts @@ -45,6 +45,8 @@ export async function initRepo( branch: string, ) { const repoPath = getRepoPath(owner, repo); + elizaLogger.info(`Repository path: ${repoPath}`); + await createReposDirectory(owner); await cloneOrPullRepository(token, owner, repo, repoPath, branch); await checkoutBranch(repoPath, branch); @@ -65,20 +67,21 @@ export async function cloneOrPullRepository( `URL: https://github.com/${owner}/${repo}.git @ branch: ${branch}`, ); - // Clone or pull repository - if (!existsSync(repoPath)) { - const git = simpleGit(); - await git.clone( - `https://${token}@github.com/${owner}/${repo}.git`, - repoPath, - { - "--branch": branch, - }, - ); - } else { - const git = simpleGit(repoPath); - await git.pull("origin", branch); + if (existsSync(repoPath)) { + // Remove existing repository if it exists + elizaLogger.info(`Removing existing repository at ${repoPath}`); + await fs.rm(repoPath, { recursive: true, force: true }); } + + // Clone repository + const git = simpleGit(); + await git.clone( + `https://${token}@github.com/${owner}/${repo}.git`, + repoPath, + { + "--branch": branch, + }, + ); } catch (error) { elizaLogger.error( `Error cloning or pulling repository ${owner}/${repo}:`, @@ -114,15 +117,41 @@ export async function writeFiles( } } +export async function getGitHubUserInfo(token: string) { + try { + const octokit = new Octokit({ + auth: token, + }); + + const { data: user } = await octokit.users.getAuthenticated(); + return { + name: user.name || user.login, + email: `${user.id}+${user.login}@users.noreply.github.com`, + }; + } catch (error) { + elizaLogger.error("Error getting GitHub user info:", error); + throw new Error(`Error getting GitHub user info: ${error}`); + } +} + export async function commitAndPushChanges( + token: string, repoPath: string, message: string, branch?: string, ): Promise { try { const git = simpleGit(repoPath); + + // Get GitHub user info + const userInfo = await getGitHubUserInfo(token); + + await git.addConfig("user.name", userInfo.name); + await git.addConfig("user.email", userInfo.email); + await git.add("."); const commit = await git.commit(message); + // biome-ignore lint/suspicious/noImplicitAnyLet: let pushResult; if (branch) { @@ -254,7 +283,7 @@ export async function retrieveFiles(repoPath: string, gitPath: string) { ignorePatterns.push(...gitignoreLines); } - elizaLogger.info(`Ignore patterns:\n${ignorePatterns.join("\n")}`); + elizaLogger.debug(`Ignore patterns:\n${ignorePatterns.join("\n")}`); const files = await glob(searchPath, { nodir: true,