Skip to content

Commit

Permalink
fix: add a new search plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
sshivaditya committed Jan 9, 2025
1 parent 36f17e9 commit dda690c
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 6 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",
"glob": "^11.0.0",
"openai": "^4.77.4",
"typebox-validators": "0.3.5"
},
Expand Down
54 changes: 51 additions & 3 deletions src/adapters/openai/helpers/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { Tool, ToolResult, ToolResultMap } from "../../../types/tool";
import { ReadFile } from "../../../tools/read-file";
import { WriteFile } from "../../../tools/write-file";
import { ExploreDir } from "../../../tools/explore-dir";
import { SearchFiles } from "../../../tools/search-files";

const MAX_TRIES = 5;
const MAX_TRIES = 20;

const sysMsg = `You are a capable AI assistant currently running on a GitHub bot.
You are designed to assist with resolving issues by making incremental fixes using a standardized tool interface.
Expand All @@ -21,7 +22,7 @@ Workflow:
To use a tool, format your response like this:
\`\`\`tool
{
"tool": "readFile|writeFile|exploreDir",
"tool": "readFile|writeFile|exploreDir|searchFiles",
"args": {
// For readFile:
"filename": "path/to/file"
Expand All @@ -32,6 +33,12 @@ To use a tool, format your response like this:
// For exploreDir:
"command": "tree"
// For searchFiles:
"pattern": "regex pattern",
"filePattern": "glob pattern (optional)",
"caseSensitive": boolean (optional),
"contextLines": number (optional)
}
}
\`\`\`
Expand Down Expand Up @@ -80,6 +87,19 @@ Available Tools:
- error?: string
- metadata: execution details
### SearchFiles Tool ###
- Purpose: Search files using regex patterns
- Method: execute(pattern: string, options?: { filePattern?: string, caseSensitive?: boolean, contextLines?: number })
- Returns: ToolResult<SearchResult> containing:
- success: boolean
- data: {
matches: Array<{ file: string, line: number, content: string, context: string[] }>,
totalFiles: number,
searchPattern: string
}
- error?: string
- metadata: execution details
Note: All file paths are relative to the current working directory. You only need to provide filenames.
Rules and Best Practices:
Expand All @@ -96,6 +116,7 @@ interface ToolSet {
readFile: ReadFile;
writeFile: WriteFile;
exploreDir: ExploreDir;
searchFiles: SearchFiles;
}

type ToolName = keyof ToolResultMap;
Expand All @@ -106,6 +127,10 @@ interface ToolRequest {
filename?: string;
content?: string;
command?: "tree";
pattern?: string;
filePattern?: string;
caseSensitive?: boolean;
contextLines?: number;
};
}

Expand All @@ -121,16 +146,18 @@ export class Completions extends SuperOpenAi {

constructor(client: OpenAI, context: Context) {
super(client, context);
this.maxTokens = 100000;
this.maxTokens = 100;
this.attempts = 0;
this.tools = {
readFile: new ReadFile(),
writeFile: new WriteFile(),
exploreDir: new ExploreDir(),
searchFiles: new SearchFiles(),
};
}

private async _executeToolRequest(request: ToolRequest, workingDir: string): Promise<ToolResult<ToolResultMap[ToolName]>> {
this.context.logger.info(`Executing tool request: ${request.tool} with args:`, request.args);
switch (request.tool) {
case "readFile":
if (!request.args.filename) throw new Error("Filename is required for readFile");
Expand All @@ -145,6 +172,14 @@ export class Completions extends SuperOpenAi {
case "exploreDir":
return this._getDirectoryTree(workingDir);

case "searchFiles":
if (!request.args.pattern) throw new Error("Search pattern is required");
return this._searchFiles(request.args.pattern, workingDir, {
filePattern: request.args.filePattern,
caseSensitive: request.args.caseSensitive,
contextLines: request.args.contextLines,
});

default:
throw new Error(`Unknown tool: ${request.tool}`);
}
Expand Down Expand Up @@ -255,6 +290,7 @@ export class Completions extends SuperOpenAi {

// Update tools with working directory
this.tools.exploreDir = new ExploreDir(workingDir);
this.tools.searchFiles = new SearchFiles(workingDir);

let isSolved = false;
let finalResponse: OpenAI.Chat.Completions.ChatCompletion | null = null;
Expand Down Expand Up @@ -323,4 +359,16 @@ export class Completions extends SuperOpenAi {
private async _getDirectoryTree(workingDir: string) {
return this._executeWithRetry(this.tools.exploreDir, "execute", workingDir, "tree");
}

private async _searchFiles(
pattern: string,
workingDir: string,
options?: {
filePattern?: string;
caseSensitive?: boolean;
contextLines?: number;
}
) {
return this._executeWithRetry(this.tools.searchFiles, "execute", workingDir, pattern, options);
}
}
105 changes: 105 additions & 0 deletions src/tools/search-files/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { glob } from "glob";
import { readFileSync } from "fs";
import { Tool, ToolResult } from "../../types/tool";

export interface SearchResult {
matches: Array<{
file: string;
line: number;
content: string;
context: string[];
}>;
totalFiles: number;
searchPattern: string;
}

export class SearchFiles implements Tool<SearchResult> {
readonly name = "search-files";
readonly description = "Searches for files and content using glob patterns and regex";

private _workingDir: string;
private readonly _contextLines = 2; // Number of lines of context before and after match

constructor(workingDir: string = process.cwd()) {
this._workingDir = workingDir;
}

async execute(
pattern: string,
options?: {
filePattern?: string;
isCaseSensitive?: boolean;
contextLines?: number;
}
): Promise<ToolResult<SearchResult>> {
try {
const filePattern = options?.filePattern || "**/*";
const isCaseSensitive = options?.isCaseSensitive ?? false;
const contextLines = options?.contextLines ?? this._contextLines;

// Find all files matching the glob pattern
const files = await glob(filePattern, {
cwd: this._workingDir,
ignore: ["**/node_modules/**", "**/.git/**"],
nodir: true,
absolute: true,
});

const regex = new RegExp(pattern, isCaseSensitive ? "g" : "gi");
const matches: SearchResult["matches"] = [];

// Search through each file
for (const file of files) {
try {
const content = readFileSync(file, "utf8");
const lines = content.split("\n");

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (regex.test(line)) {
// Get context lines
const start = Math.max(0, i - contextLines);
const end = Math.min(lines.length, i + contextLines + 1);
const context = lines.slice(start, end);

matches.push({
file: file.replace(this._workingDir + "/", ""),
line: i + 1,
content: line,
context,
});
}
regex.lastIndex = 0; // Reset regex state
}
} catch (error) {
console.error(`Error reading file ${file}:`, error);
// Continue with other files
}
}

return {
success: true,
data: {
matches,
totalFiles: files.length,
searchPattern: pattern,
},
metadata: {
timestamp: Date.now(),
toolName: this.name,
filePattern,
contextLines,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
metadata: {
timestamp: Date.now(),
toolName: this.name,
},
};
}
}
}
12 changes: 12 additions & 0 deletions src/types/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,22 @@ export interface TerminalCommandResult {
command: string;
}

export interface SearchResult {
matches: Array<{
file: string;
line: number;
content: string;
context: string[];
}>;
totalFiles: number;
searchPattern: string;
}

// Tool result type mapping
export type ToolResultMap = {
readFile: FileReadResult;
writeFile: FileWriteResult;
exploreDir: DirectoryExploreResult;
terminal: TerminalCommandResult;
searchFiles: SearchResult;
};
Loading

0 comments on commit dda690c

Please sign in to comment.