Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terminal support for multi-turn conversations #9

Merged
merged 61 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
67ba81a
Improved text wrapping in ConversationLogger utility
cngarrison Jul 29, 2024
2923b4f
Commit message:
cngarrison Jul 29, 2024
a7fe40a
console width
cngarrison Jul 29, 2024
d48ed4e
Improved text wrapping in ConversationLogger utility
cngarrison Jul 29, 2024
2b331ff
Simplified the ConversationLogger class by removing unnecessary forma…
cngarrison Jul 29, 2024
2641bb7
Added ANSI color constants and log formatting functionality to the `L…
cngarrison Jul 29, 2024
4768199
Refactored log display functionality to use BufReader and handle file…
cngarrison Jul 29, 2024
fe2ff93
Moved the `displayFormattedLogs` function to a separate utility file …
cngarrison Jul 29, 2024
0ddadf3
Implemented streaming log file reading and added support for followin…
cngarrison Jul 29, 2024
a0baadc
Implemented log entry separator and added functions to display and wr…
cngarrison Jul 29, 2024
dfee239
Added log entry separator and trimmed message in conversation logger
cngarrison Jul 29, 2024
ea5a7c2
Improved log file reading and processing in `logFormatter.utils.ts`
cngarrison Jul 29, 2024
7b7dc1d
Refactored log file reading and processing logic in `logFormatter.uti…
cngarrison Jul 29, 2024
9947c9d
Revised log entry separator in `logFormatter.utils.ts`
cngarrison Jul 29, 2024
ce281a0
Improved log viewing options for chat and API logs
cngarrison Jul 29, 2024
47fec88
diff better
cngarrison Jul 29, 2024
95da022
Improved log entry separator handling and added a function to count l…
cngarrison Jul 29, 2024
1eee46a
Added a new chat command to the CLI that allows users to start a chat…
cngarrison Jul 29, 2024
403adea
Replaced `conversationStart` command with `chat` command in CLI
cngarrison Jul 29, 2024
78e722a
Added syntax highlighting to API client response output
cngarrison Jul 29, 2024
3cb6db5
Added a new import for the `cliffy/prompt` module in the `deno.jsonc`…
cngarrison Jul 29, 2024
e37d091
cleaning
cngarrison Jul 29, 2024
4a35fd0
Removed the `handleConversationOutput` method from the `ApiClient` class
cngarrison Jul 29, 2024
c519e28
Refactored conversation output handling in chat command
cngarrison Jul 29, 2024
0cecd4f
Added a new method `getCurrentCommit` to the `GitUtils` class to retr…
cngarrison Jul 29, 2024
742940f
The changes in this commit can be summarized as follows:
cngarrison Jul 29, 2024
ce3cdd2
Formatted conversation summary with ANSI colors and improved layout
cngarrison Jul 29, 2024
d45920c
Improved log formatter utility to handle multi-line messages more eff…
cngarrison Jul 29, 2024
21ef09d
Refactored conversation summary display to use a more compact and vis…
cngarrison Jul 29, 2024
49ffb44
Added checks for API running status and start/stop API commands in co…
cngarrison Jul 29, 2024
895a29c
Added API start and stop functionality to the `conversationStart` com…
cngarrison Jul 29, 2024
b037ea1
Added project title to project editor response
cngarrison Jul 29, 2024
d97b9cc
Added `title` field to `ConversationResponse` interface in `conversat…
cngarrison Jul 29, 2024
787046a
Commit message:
cngarrison Jul 29, 2024
5736a6f
Added a new function `handleConversationCompleteUpdated` to handle th…
cngarrison Jul 29, 2024
3540a2f
Added prompt history utilities for the BBAI CLI
cngarrison Jul 29, 2024
6b8a41b
Added multiline input with prompt history and file suggestions to the…
cngarrison Jul 29, 2024
dbf100b
history of the prompt
cngarrison Jul 30, 2024
1b5b243
Fix request caching with "key too large" error
cngarrison Jul 30, 2024
bd2b3c5
proper project root
cngarrison Jul 30, 2024
d03f154
Improved error handling in conversationStart command
cngarrison Jul 30, 2024
ffeccff
Added a new LLMTool for search and replace operations
cngarrison Jul 31, 2024
efa63a4
Added a new search and replace tool to the project editor
cngarrison Jul 31, 2024
16e49ec
Improved error message for failed patch application
cngarrison Jul 31, 2024
b262eb8
search and replace tool, conversation terminal updates...
cngarrison Jul 31, 2024
7fc8c5b
Added debug logging to the `displayFormattedLogs` function in the `lo…
cngarrison Jul 31, 2024
5cb0c4f
Improved text wrapping in log formatter utility
cngarrison Jul 31, 2024
3149b85
Improved log formatting and debugging output
cngarrison Jul 31, 2024
4185965
Commit message:
cngarrison Jul 31, 2024
5dc2fd5
Improved text wrapping in log formatter utility
cngarrison Jul 31, 2024
488df88
Improved text wrapping in log formatter utility
cngarrison Jul 31, 2024
2ff5fce
Trimmed whitespace from log entries and ensured log entries end with …
cngarrison Jul 31, 2024
a006e9b
Improved log formatter to handle long words and maintain consistent i…
cngarrison Jul 31, 2024
32c5b05
Improved log formatting and handling:
cngarrison Jul 31, 2024
e23d702
use file listing rather than ctags, log formatting fixes
cngarrison Jul 31, 2024
7f2e09c
Implemented a check to ensure that search and replace strings are dif…
cngarrison Jul 31, 2024
770b8c3
Improved search and replace operation handling in project editor:
cngarrison Jul 31, 2024
f233c7a
Log chats with conversation
cngarrison Jul 31, 2024
6eec435
Added `apiRestart` command to the CLI
cngarrison Aug 1, 2024
5652d98
Added `apiRestart` command to stop and start the API
cngarrison Aug 1, 2024
13e31e8
Keep typescript happy plus version bump
cngarrison Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]


## [0.0.5-alpha] - 2024-08-01

### Changed

- Added terminal support for multi-turn conversations
- Applied formatting to chat logs for easier reading


## [0.0.4-alpha] - 2024-07-28

### Changed
Expand Down
2 changes: 1 addition & 1 deletion api/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bbai-api",
"version": "0.0.4-alpha",
"version": "0.0.5-alpha",
"exports": "./src/main.ts",
"tasks": {
"start": "deno run --allow-read --allow-write --allow-run --allow-net --allow-env src/main.ts",
Expand Down
19 changes: 18 additions & 1 deletion api/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

180 changes: 158 additions & 22 deletions api/src/editor/projectEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,15 @@ export class ProjectEditor {
const projectInfo: ProjectInfo = { type: 'empty', content: '', tier: null };

const bbaiDir = await this.getBbaiDir();
/*
await generateCtags(this.bbaiDir, this.projectRoot);
const ctagsContent = await readCtagsFile(bbaiDir);
if (ctagsContent) {
projectInfo.type = 'ctags';
projectInfo.content = ctagsContent;
projectInfo.tier = 0; // Assuming ctags is always tier 0
}
*/

if (projectInfo.type === 'empty') {
const projectRoot = await this.getProjectRoot();
Expand Down Expand Up @@ -202,7 +204,7 @@ export class ProjectEditor {

const applyPatchTool: LLMTool = {
name: 'apply_patch',
description: 'Apply a patch to a file',
description: 'Apply a well-formed patch to a file',
input_schema: {
type: 'object',
properties: {
Expand All @@ -212,13 +214,48 @@ export class ProjectEditor {
},
patch: {
type: 'string',
description: 'The patch to be applied in diff format',
description: 'The carefully written and well-formed patch to be applied in unified diff format',
},
},
required: ['filePath', 'patch'],
},
};

const searchAndReplaceTool: LLMTool = {
name: 'search_and_replace',
description: 'Apply a list of search and replace operations to a file',
input_schema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'The path of the file to be modified',
},
operations: {
type: 'array',
items: {
type: 'object',
properties: {
search: {
type: 'string',
description:
'The exact literal text to search for, with all leading and trailing whitespace from the original file',
},
replace: {
type: 'string',
description:
'The text to replace with, matching the same indent level as the original file',
},
},
required: ['search', 'replace'],
},
description: 'List of literal search and replace operations to apply',
},
},
required: ['filePath', 'operations'],
},
};

const searchProjectTool: LLMTool = {
name: 'search_project',
description: 'Search the project for files matching a pattern',
Expand All @@ -240,8 +277,8 @@ export class ProjectEditor {
};

this.conversation?.addTool(requestFilesTool);
//this.conversation?.addTool(vectorSearchTool);
this.conversation?.addTool(applyPatchTool);
this.conversation?.addTool(searchAndReplaceTool);
this.conversation?.addTool(searchProjectTool);
}

Expand Down Expand Up @@ -273,7 +310,7 @@ export class ProjectEditor {
return conversation;
}
async createChat(): Promise<LLMChatInteraction> {
const chat = new LLMChatInteraction(this.llmProviderFast);
const chat = new LLMChatInteraction(this.llmProviderFast, this.conversation?.id);
await chat.init();
return chat;
}
Expand Down Expand Up @@ -484,6 +521,7 @@ export class ProjectEditor {
statementCount: this.statementCount,
turnCount,
totalTurnCount: this.totalTurnCount,
title: this.conversation?.title || '',
};
}

Expand All @@ -493,6 +531,16 @@ export class ProjectEditor {
): Promise<string> {
let feedback = '';
switch (tool.toolName) {
case 'search_project': {
const { pattern, file_pattern } = tool.toolInput as { pattern: string; file_pattern?: string };
const repoSearchResults = await this.handleSearchProject(
pattern,
file_pattern || undefined,
tool.toolUseId,
);
feedback = `Project search completed. ${repoSearchResults.length} files found matching the pattern.`;
break;
}
case 'request_files': {
const fileNames = (tool.toolInput as { fileNames: string[] }).fileNames;
const filesAdded = await this.handleRequestFiles(fileNames, tool.toolUseId || '');
Expand All @@ -513,14 +561,13 @@ export class ProjectEditor {
feedback = `Patch applied successfully to file: ${filePath}`;
break;
}
case 'search_project': {
const { pattern, file_pattern } = tool.toolInput as { pattern: string; file_pattern?: string };
const repoSearchResults = await this.handleSearchProject(
pattern,
file_pattern || undefined,
tool.toolUseId,
);
feedback = `Project search completed. ${repoSearchResults.length} files found matching the pattern.`;
case 'search_and_replace': {
const { filePath, operations } = tool.toolInput as {
filePath: string;
operations: Array<{ search: string; replace: string }>;
};
await this.handleSearchAndReplace(filePath, operations, tool.toolUseId);
feedback = `Search and replace operations applied successfully to file: ${filePath}`;
break;
}
default: {
Expand Down Expand Up @@ -550,7 +597,6 @@ export class ProjectEditor {
try {
const { files, error: _error } = await searchFiles(this.projectRoot, pattern, file_pattern);

// Add tool result message
const resultMessage = `Found ${files.length} files matching the pattern "${pattern}"${
file_pattern ? ` with file pattern "${file_pattern}"` : ''
}:\n${files.join('\n')}`;
Expand All @@ -567,9 +613,98 @@ export class ProjectEditor {
}
}

async handleSearchAndReplace(
filePath: string,
operations: Array<{ search: string; replace: string }>,
toolUseId: string,
): Promise<void> {
if (!this.isPathWithinProject(filePath)) {
throw createError(ErrorType.FileHandling, `Access denied: ${filePath} is outside the project directory`, {
name: 'search-and-replace',
filePath,
operation: 'search-replace',
} as FileHandlingErrorOptions);
}

const fullFilePath = join(this.projectRoot, filePath);
logger.info(`Handling search and replace for file: ${fullFilePath}`);

try {
let content = await Deno.readTextFile(fullFilePath);

let changesMade = false;
let allOperationsSkipped = true;
for (const operation of operations) {
const { search, replace } = operation;

// Validate that search and replace strings are different
if (search === replace) {
const warningMessage = `Warning: Search and replace strings are identical for operation: ${
JSON.stringify(operation)
}. Operation skipped.`;
logger.warn(warningMessage);
this.conversation?.addMessageForToolResult(toolUseId, warningMessage, true);
continue; // Skip this operation
}

const originalContent = content;
content = content.replaceAll(search, replace);

// Check if the content actually changed
if (content !== originalContent) {
changesMade = true;
allOperationsSkipped = false;
}
}

if (changesMade) {
await Deno.writeTextFile(fullFilePath, content);
this.patchedFiles.add(filePath);

// Log the applied changes
if (this.conversation) {
logger.info(`Saving conversation search and replace: ${this.conversation.id}`);
const persistence = new ConversationPersistence(this.conversation.id, this);
await persistence.logPatch(filePath, JSON.stringify(operations));
await this.createCommitAfterPatching();
}

// Add success tool result message
this.conversation?.addMessageForToolResult(
toolUseId,
`Search and replace operations applied successfully to file: ${filePath}`,
);
} else {
const noChangesMessage = allOperationsSkipped
? `No changes were made to the file: ${filePath}. All operations were skipped due to identical search and replace strings.`
: `No changes were made to the file: ${filePath}. The search strings were not found in the file content.`;
logger.info(noChangesMessage);
this.conversation?.addMessageForToolResult(toolUseId, noChangesMessage, true);
}

this.conversation?.addMessageForToolResult(
toolUseId,
`Search and replace operations applied successfully to file: ${filePath}`,
);
} catch (error) {
let errorMessage = `Failed to apply search and replace to ${filePath}: ${error.message}`;
logger.error(errorMessage);

// Add error tool result message
this.conversation?.addMessageForToolResult(toolUseId, errorMessage, true);

throw createError(ErrorType.FileHandling, errorMessage, {
name: 'search-and-replace',
filePath: filePath,
operation: 'search-replace',
} as FileHandlingErrorOptions);
}
}

async handleApplyPatch(filePath: string, patch: string, toolUseId: string): Promise<void> {
if (!this.isPathWithinProject(filePath)) {
throw createError(ErrorType.FileHandling, `Access denied: ${filePath} is outside the project directory`, {
name: 'apply-patch',
filePath,
operation: 'patch',
} as FileHandlingErrorOptions);
Expand Down Expand Up @@ -612,14 +747,14 @@ export class ProjectEditor {
});

if (patchedContent === false) {
throw createError(
ErrorType.FileHandling,
'Failed to apply patch. The patch does not match the current file content.',
{
filePath,
operation: 'patch',
} as FileHandlingErrorOptions,
);
const errorMessage =
'Failed to apply patch. The patch does not match the current file content. ' +
'Consider using the `search_and_replace` tool for more precise modifications.';
throw createError(ErrorType.FileHandling, errorMessage, {
name: 'apply-patch',
filePath,
operation: 'patch',
} as FileHandlingErrorOptions);
}

await Deno.writeTextFile(fullFilePath, patchedContent);
Expand All @@ -635,7 +770,6 @@ export class ProjectEditor {
await this.createCommitAfterPatching();
}

// Add tool result message
this.conversation?.addMessageForToolResult(toolUseId, `Patch applied successfully to file: ${filePath}`);
} catch (error) {
let errorMessage: string;
Expand All @@ -652,6 +786,7 @@ export class ProjectEditor {
this.conversation?.addMessageForToolResult(toolUseId, errorMessage, true);

throw createError(ErrorType.FileHandling, errorMessage, {
name: 'apply-patch',
filePath: filePath,
operation: 'patch',
} as FileHandlingErrorOptions);
Expand Down Expand Up @@ -799,6 +934,7 @@ export class ProjectEditor {
async updateFile(filePath: string, _content: string): Promise<void> {
if (!this.isPathWithinProject(filePath)) {
throw createError(ErrorType.FileHandling, `Access denied: ${filePath} is outside the project directory`, {
name: 'update-file',
filePath,
operation: 'write',
} as FileHandlingErrorOptions);
Expand Down
Loading
Loading