Skip to content

Commit

Permalink
feat: add explore dir tool
Browse files Browse the repository at this point in the history
  • Loading branch information
sshivaditya committed Jan 9, 2025
1 parent 0eebccc commit a35576a
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 63 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@sinclair/typebox": "0.32.33",
"@ubiquity-dao/ubiquibot-logger": "^1.3.0",
"dotenv": "16.4.5",
"openai": "^4.77.4",
"typebox-validators": "0.3.5"
},
"devDependencies": {
Expand Down
67 changes: 67 additions & 0 deletions src/adapters/openai/helpers/completions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import OpenAI from "openai";
import { Context } from "../../../types/context";
import { SuperOpenAi } from "./openai";

const sysMsg = `You are a capable AI assistant currently running on a GitHub bot.
You are designed to assist with repository maintenance, code reviews, and issue resolution.
You have access to the following tools:
- read_file: Read contents of a file in the repository
- write_file: Write/update contents to a file
- terminal: Execute terminal commands
- test_code: Run tests for the codebase
Always be professional, concise, and follow these rules:
1. Before making changes, understand the context fully
2. When modifying code, ensure it maintains existing functionality
3. Follow the project's coding style and conventions
4. Document any significant changes
5. Consider edge cases and error handling`;

export class Completions extends SuperOpenAi {
protected context: Context;
protected model: string;
protected maxTokens: number;

constructor(client: OpenAI, context: Context) {
super(client, context);
this.context = context;
this.model = "claude/sonnet";
this.maxTokens = 100;
}

async createCompletion(prompt: string) {
const res: OpenAI.Chat.Completions.ChatCompletion = await this.client.chat.completions.create({
model: this.model,
messages: [
{
role: "system",
content: [
{
type: "text",
text: sysMsg,
},
],
},
{
role: "user",
content: [
{
type: "text",
text: prompt,
},
],
},
],
temperature: 0.2,
max_tokens: this.maxTokens,
top_p: 0.5,
frequency_penalty: 0,
presence_penalty: 0,
response_format: {
type: "text",
},
});

return res;
}
}
11 changes: 11 additions & 0 deletions src/adapters/openai/helpers/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OpenAI } from "openai";
import { Context } from "../../../types/context";

export class SuperOpenAi {
protected client: OpenAI;
protected context: Context;
constructor(client: OpenAI, context: Context) {
this.client = client;
this.context = context;
}
}
79 changes: 19 additions & 60 deletions src/handlers/front-controller.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,28 @@
import { ExploreDir } from "../tools/explore-dir";
import { Context } from "../types";
import { sayHello } from "./say-hello";

/**
* NOTICE: Remove this file or use it as a template for your own plugins.
*
* This encapsulates the logic for a plugin if the only thing it does is say "Hello, world!".
*
* Try it out by running your local kernel worker and running the `yarn worker` command.
* Comment on an issue in a repository where your GitHub App is installed and see the magic happen!
*
* Logger examples are provided to show how to log different types of data.
*/
export async function delegate(context: Context) {
const { logger, payload, octokit } = context;

const sender = payload.comment.user?.login;
const { logger, payload } = context;
const body = payload.comment.body;
const repo = payload.repository.name;
const issueNumber = payload.issue.number;
const owner = payload.repository.owner.login;
const body = payload.comment.body;

logger.debug(`Executing decideHandler:`, { sender, repo, issueNumber, owner });

const targetUser = body.match(/^\/\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))/i);
if (!targetUser) {
logger.error(`Missing target username from comment: ${body}`);
return;
}
const personalAgentOwner = targetUser[0].replace("/@", "");

logger.info(`Comment received:`, { owner, personalAgentOwner, comment: body });

let reply;

if (body.match(/^\/\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))\s+say\s+hello/i)) {
reply = sayHello();
} else {
reply = "I could not understand your comment to give you a quick response. I will get back to you later.";
logger.error(`Invalid command.`, { body });
}

const replyWithQuote = ["> ", `${body}`, "\n\n", reply].join("");
const issueNumber = payload.issue.number;

try {
await octokit.issues.createComment({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: payload.issue.number,
body: replyWithQuote,
});
} catch (error) {
/**
* logger.fatal should not be used in 9/10 cases. Use logger.error instead.
*
* Below are examples of passing error objects to the logger, only one is needed.
*/
if (error instanceof Error) {
logger.error(`Error creating comment:`, { error: error, stack: error.stack });
throw error;
} else {
logger.error(`Error creating comment:`, { err: error, error: new Error() });
throw error;
}
// Check if the comment is requesting to solve the issue
if (body.toLowerCase().includes("solve this issue")) {
// Initialize LLM tools
const explore = new ExploreDir();
// Get the current directory tree
let tree = await explore.current_dir_tree();
// Log the tree
logger.info(tree);
// Clone the repository and get the file tree
await explore.clone_repo(repo, owner, issueNumber);
// Log the tree again
tree = await explore.current_dir_tree();
logger.info(tree);
}

logger.ok(`Comment created: ${reply}`);
logger.verbose(`Exiting decideHandler`);
logger.ok(`Comment processed: ${body}`);
logger.verbose(`Exiting delegate`);
}
3 changes: 0 additions & 3 deletions src/handlers/say-hello.ts

This file was deleted.

78 changes: 78 additions & 0 deletions src/tools/explore-dir/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { commonCallBack, Terminal } from "../terminal";

export class ExploreDir {
private _shellInterface: Terminal;

constructor() {
this._shellInterface = new Terminal(commonCallBack("stdout"), commonCallBack("stderr"), commonCallBack("exit"));
}

current_dir_tree(): Promise<string> {
return new Promise((resolve, reject) => {
let output = "";
let isCompleted = false;

this._shellInterface.runCommand("ls -R");

const stdout = this._shellInterface.outputOnStdout();
if (!stdout) {
reject(new Error("No bash instance running"));
return;
}
stdout.on("data", (data: string) => {
output += data.toString();
if (isCompleted) {
resolve(output);
}
});

this._shellInterface.hasCommandCompleted();
stdout.on("data", (data: string) => {
const exitCode = parseInt(data.toString().trim());
if (exitCode === 0) {
isCompleted = true;
if (output) {
resolve(output);
}
} else {
reject(new Error(`Command failed with exit code ${exitCode}`));
}
});
});
}

clone_repo(repo: string, owner: string, issueNumber: number): Promise<string> {
return new Promise((resolve, reject) => {
let output = "";
let isCompleted = false;
const tmpDir = `/tmp/repo-${owner}-${repo}-${issueNumber}`;

this._shellInterface.runCommand(`git clone [email protected]:${owner}/${repo}.git ${tmpDir}`);

const stdout = this._shellInterface.outputOnStdout();
if (!stdout) {
reject(new Error("No bash instance running"));
return;
}
stdout.on("data", (data: string) => {
output += data.toString();
if (isCompleted) {
resolve(output);
}
});

this._shellInterface.hasCommandCompleted();
stdout.on("data", (data: string) => {
const exitCode = parseInt(data.toString().trim());
if (exitCode === 0) {
isCompleted = true;
if (output) {
resolve(output);
}
} else {
reject(new Error(`Git clone failed with exit code ${exitCode}`));
}
});
});
}
}
125 changes: 125 additions & 0 deletions src/tools/terminal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { ChildProcessWithoutNullStreams, spawn } from "child_process";

const BASH_ERR_MSG = "No bash instance running.";

export class Terminal {
private _process: ChildProcessWithoutNullStreams | null;
private _onStdout: (data: string) => void;
private _onStderr: (data: string) => void;
private _onClose: (code: string) => void;

constructor(onStdout: (data: string) => void, onStderr: (data: string) => void, onClose: (code: string) => void) {
this._process = null;
this.start();
this._onStdout = onStdout;
this._onStderr = onStderr;
this._onClose = onClose;
}

// Method to start a bash instance
start() {
this._process = spawn("bash", [], {
stdio: ["pipe", "pipe", "pipe"],
});

this._process.stdout.on("data", (data: string) => {
this._onStdout(data);
});

this._process.stderr.on("data", (data: string) => {
this._onStderr(data);
});

this._process.on("close", (code: string) => {
this._onClose(code);
});
}

// Method to run a command in the bash instance
runCommand(command: string) {
if (this._process) {
this._process.stdin.write(`${command}\n`);
} else {
console.error(BASH_ERR_MSG);
}
}

hasCommandCompleted() {
if (this._process) {
this._process.stdin.write("echo $?");
} else {
console.error(BASH_ERR_MSG);
}
}

outputOnStdout() {
if (this._process) {
return this._process.stdout;
} else {
console.error(BASH_ERR_MSG);
}
}

// Method to kill the bash instance
kill() {
if (this._process) {
this._process.kill();
this._process = null;
console.log("Bash instance killed.");
} else {
console.error(BASH_ERR_MSG);
}
}
}

export function commonCallBack(id: string) {
return (data: string) => {
console.log(`Terminal ${id} stdout: ${data}`);
};
}

export class TerminalManager {
private _terminals: Map<string, Terminal>;
constructor() {
this._terminals = new Map();
this.init();
}

init() {
console.log("TerminalManager initialized");
}

// Method to create a new terminal instance
createTerminal(id: string) {
if (this._terminals.has(id)) {
console.error(`Terminal with id ${id} already exists.`);
return;
}
const terminal = new Terminal(commonCallBack(id), commonCallBack(id), commonCallBack(id));
this._terminals.set(id, terminal);
terminal.start();
console.log(`Terminal with id ${id} created.`);
}

// Method to run a command in a specific terminal instance
runCommandInTerminal(id: string, command: string) {
const terminal = this._terminals.get(id);
if (terminal) {
terminal.runCommand(command);
} else {
console.error(`Terminal with id ${id} does not exist.`);
}
}

// Method to kill a specific terminal instance
killTerminal(id: string) {
const terminal = this._terminals.get(id);
if (terminal) {
terminal.kill();
this._terminals.delete(id);
console.log(`Terminal with id ${id} killed.`);
} else {
console.error(`Terminal with id ${id} does not exist.`);
}
}
}
Loading

0 comments on commit a35576a

Please sign in to comment.