Skip to content

Commit

Permalink
feat(shell-tool): improve bash tool
Browse files Browse the repository at this point in the history
- fix critical error handling in async
- add user prefs config to variable system message
- add --new-chat option to start anew without direct adding prev conversations to chat history
  • Loading branch information
erik-balfe committed Nov 25, 2024
1 parent 7be6eb1 commit bc2c69a
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 46 deletions.
15 changes: 10 additions & 5 deletions src/ai/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getCorrectness, getFaithfulness, getRelevancy } from "../features/userS
import { getUserEvaluationScore } from "../features/userScore/getUserEvaluationScore";
import { askUserCallback, UserCliResponse } from "../tools/askUser";
import { createExecuteCommandTool } from "../tools/executeCommand";
import { createWaitTool } from "../tools/wait";
import { AppConfig, loadConfig } from "../utils/config";
import { countUsageCost } from "../utils/countUsageCost";
import {
Expand All @@ -27,16 +28,19 @@ import { constructChatHistory } from "./chatHistory";
import { getAiAgent } from "./getAiAgent";
import { saveConversationDocument } from "./memories/memories";
import { parseAgentMessage, ParsedAgentMessage } from "./parseAgentResponseContent";
import { createWaitTool } from "../tools/wait";

export async function getConversationId(db: Database, userQuery: string): Promise<number> {
export async function getConversationId(
db: Database,
userQuery: string,
newConversation: boolean,
): Promise<number> {
logger.debug("Fetching last interaction record from the database.");
const lastInteractionRecord = getLastConversationEntry(db);
logger.debug(`Last interaction record: ${JSON.stringify(lastInteractionRecord)}`);

let conversationId: number;

if (!lastInteractionRecord?.conversationId) {
if (newConversation || !lastInteractionRecord?.conversationId) {
logger.debug("No previous interaction found. Creating a new conversation entry.");
conversationId = await insertConversation(db, userQuery, Date.now());
logger.debug(`New conversation ID created: ${conversationId}`);
Expand Down Expand Up @@ -65,6 +69,7 @@ export async function agentLoop(
db: Database,
appConfig: AppConfig,
contextAllocation: ContextAllocation,
newConversation: boolean,
) {
let questionForUser: string = "What you your task or question to Ai-console-agent?:";
let userQuery = consoleInput;
Expand Down Expand Up @@ -96,7 +101,7 @@ export async function agentLoop(
continue;
}

const conversationId = await getConversationId(db, userQuery);
const conversationId = await getConversationId(db, userQuery, newConversation);
logger.debug(`Conversation ID: ${conversationId}`);
logger.debug(`Conversation ID raw: ${JSON.stringify(conversationId)}`);
if (!conversationId) {
Expand Down Expand Up @@ -134,7 +139,7 @@ export async function agentLoop(
userQuery = userMessage.answer;
}
if (userMessage.cancelled) {
console.log("Saving conversation data and exiting");
console.log("Saving conversation data");
}
} while (!agentMessage?.taskComplete && !userMessage?.cancelled && !userMessage.exitProgram);

Expand Down
12 changes: 8 additions & 4 deletions src/ai/buildSystemMessage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { DynamicContextData } from "../cli/contextUtils";
import { CONFIG_DIR_PATH } from "../constants";
import { ConfigWithMetadata } from "../utils/config";
import { AsyncCommandTracker } from "./asyncCommandTracker";

export const buildConstantSystemMessage = () =>
Expand Down Expand Up @@ -44,7 +46,8 @@ Usage Strategy:
- Use Scratch Space as a clipboard for any text that might be needed later
Command Handling and Monitoring:
- **Async Command Execution**: When executing long-running commands, use async mode to run commands without blocking. The command's output will be written to files, allowing for monitoring.
- **Async Command Execution**: When executing long-running commands, use async mode to run commands that may take a while without blocking. The command's output will be written to files, allowing for monitoring.
- All usual commands that expected to complete in a few moments should be executed in sync mode.
- **Command Status Tracking**: Track the state of currently running async commands. You will be notified of their status changes, including success or failure.
- **Wait Tool**: Use the wait tool to pause execution for a specified time. It can also track the state of running async commands, allowing you to be informed if a command fails before your wait period ends.
- While working with async commands, consider checking their status regularly. You can utilize tail, grep, and wait tools to monitor the progress effectively.
Expand Down Expand Up @@ -72,6 +75,7 @@ Evaluation Process:
User Profile Management:
- Update user preferences based on interactions.
- Regularly assess and record user traits for effective future engagements.
- User profile (user config) is a file with user prefecences placed in ai-console-agent settings directory in ${CONFIG_DIR_PATH}
Task Approach:
1. Assess the clarity of tasks (1-10 scale). If the score is < 7, gather more information.
Expand Down Expand Up @@ -127,7 +131,7 @@ Memories are presented here in some special format as XML tags with some metadat
export function bulidVariableSystemMessage(
runDir: string,
{ pwdOutput, lsOutput, time }: DynamicContextData,
config: object,
config: ConfigWithMetadata,
userQuery: string,
) {
const tracker = AsyncCommandTracker.getInstance();
Expand Down Expand Up @@ -155,8 +159,8 @@ Here's an overview of your operating environment:
- Current directory: "${pwdOutput}"
- Current sharp time: ${time}
- Current directory contents: "${lsOutput}"
- Current Configs:
${JSON.stringify(config, null, 2)}
- Current user config (user prefecences):
${config.rawConfigs.user}
${commandStatus}
</environment>
`.trim();
Expand Down
12 changes: 9 additions & 3 deletions src/cli/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ export interface ParsedArguments {
setLogLevel?: LogLevelType;
getLogLevel: boolean;
model?: string;
showKeys: boolean;
showAPIKeys: boolean;
newConversation: boolean
}
export function parseArguments(args: string[]): ParsedArguments {
const parsedArgs: ParsedArguments = {
input: "",
resetKey: false,
showHelp: false,
getLogLevel: false,
showKeys: false,
showAPIKeys: false,
newConversation: false,
};

for (let i = 2; i < args.length; i++) {
Expand All @@ -33,11 +35,14 @@ export function parseArguments(args: string[]): ParsedArguments {
parsedArgs.showHelp = true;
break;
case "--show-keys":
parsedArgs.showKeys = true;
parsedArgs.showAPIKeys = true;
break;
case "--get-log-level":
parsedArgs.getLogLevel = true;
break;
case "--new-chat":
parsedArgs.newConversation = true;
break;
default:
if (arg.startsWith("--log-level=") || arg.startsWith("--set-log-level=")) {
const levelString = arg.split("=")[1].toUpperCase() as keyof typeof LogLevel;
Expand Down Expand Up @@ -78,6 +83,7 @@ export function printHelp() {
console.log(" --get-log-level Display the current log level");
console.log(" --model=<model> Set the AI model to use (can be model ID or shortname)");
console.log(" --show-keys Display stored API keys in a safe format");
console.log(" --new-chat Start a new chat");

console.log("\nAvailable Models:");

Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async function main() {
let db;

try {
const { input, resetKey, showHelp, setLogLevel, getLogLevel, model, showKeys } = parseArguments(Bun.argv);
const { input, resetKey, showHelp, setLogLevel, getLogLevel, model, showAPIKeys, newConversation } = parseArguments(Bun.argv);

const { appConfig, userPrefs } = loadConfig();

Expand Down Expand Up @@ -55,7 +55,7 @@ async function main() {

logger.setLevel(appConfig.logLevel);

if (showKeys) {
if (showAPIKeys) {
const keys = await getAllStoredKeys();
console.log(chalk.cyan("\nStored API Keys:"));
for (const [provider, key] of Object.entries(keys)) {
Expand Down Expand Up @@ -94,7 +94,7 @@ async function main() {
logger.debug("Starting user interaction loop");
let userQuery = input;
logger.debug(`Initial user query: ${userQuery}`);
await agentLoop(userQuery, db, appConfig, contextAllocation);
await agentLoop(userQuery, db, appConfig, contextAllocation, newConversation);
} catch (error) {
if (error instanceof APIError) {
if (error.status === 401) {
Expand Down
2 changes: 1 addition & 1 deletion src/tools/toolMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function createToolMiddleware(db: Database, conversationId: number) {
let executionTime = 0;

try {
logger.debug(
logger.warn(
`Starting tool: "${toolName}", ID: "${toolCallId}" with arguments: "${JSON.stringify(params)}"`,
);
result = await toolFunction(params);
Expand Down
126 changes: 97 additions & 29 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { LogLevel, LogLevelType } from "./logger";
export interface AppConfig {
logLevel: LogLevelType;
model: string;
contextLimitMultiplier: number;
// ... other app settings,
}

export interface UserPreferences {
export interface UserPreferences extends Record<string, any> {
techExpertiseLevel: number;
preferredName?: string;
// ... other user preferences
Expand All @@ -18,28 +19,66 @@ export interface UserPreferences {
const DEFAULT_APP_CONFIG: AppConfig = {
logLevel: LogLevel.WARN,
model: Object.values(MODELS).find((model) => model.default)?.id || "",
contextLimitMultiplier: 1,
};

const DEFAULT_USER_PREFS: UserPreferences = {
techExpertiseLevel: 2,
techExpertiseLevel: 0.5,
};

function parseConfigFile(filePath: string): Record<string, string> {
// Add new interfaces
interface ParsedConfigLine {
key?: string;
value?: string;
comment?: string;
raw: string;
}

interface ConfigData {
parsed: Record<string, string>;
raw: string;
lines: ParsedConfigLine[];
}

function parseConfigFile(filePath: string): ConfigData {
const content = fs.readFileSync(filePath, "utf8");
const lines = content.split("\n");
const config: Record<string, string> = {};
const parsed: Record<string, string> = {};
const parsedLines: ParsedConfigLine[] = [];

for (const line of lines) {
const parsedLine: ParsedConfigLine = { raw: line };
const trimmedLine = line.trim();
if (trimmedLine && !trimmedLine.startsWith("#")) {
const [key, value] = trimmedLine.split("=").map((part) => part.trim());
if (key && value) {
config[key] = value;

if (trimmedLine) {
// Handle pure comment lines
if (trimmedLine.startsWith("#")) {
parsedLine.comment = trimmedLine.substring(1).trim();
} else {
// Handle config lines with potential inline comments
const [configPart, ...commentParts] = line.split("#");
const [key, value] = configPart.split("=").map((part) => part.trim());

if (key && value) {
parsed[key] = value;
parsedLine.key = key;
parsedLine.value = value;
}

if (commentParts.length > 0) {
parsedLine.comment = commentParts.join("#").trim();
}
}
}

parsedLines.push(parsedLine);
}

return config;
return {
parsed,
raw: content,
lines: parsedLines,
};
}

function writeConfigFile(filePath: string, config: Record<string, any>): void {
Expand Down Expand Up @@ -96,45 +135,81 @@ function execCommand(command: string, cwd: string): string {
// }
// }

export function loadConfig(): { appConfig: AppConfig; userPrefs: UserPreferences } {
export interface ConfigWithMetadata {
appConfig: AppConfig;
userPrefs: UserPreferences;
rawConfigs: {
app: string;
user: string;
};
}

export function loadConfig(): ConfigWithMetadata {
if (!fs.existsSync(CONFIG_DIR_PATH)) {
fs.mkdirSync(CONFIG_DIR_PATH, { recursive: true });
}

let appConfig: AppConfig = DEFAULT_APP_CONFIG;
let userPrefs: UserPreferences = DEFAULT_USER_PREFS;
let rawAppConfig = "";
let rawUserConfig = "";

try {
if (fs.existsSync(APP_CONFIG_FILE_PATH)) {
const parsedConfig = parseConfigFile(APP_CONFIG_FILE_PATH);
const appConfigData = parseConfigFile(APP_CONFIG_FILE_PATH);
rawAppConfig = appConfigData.raw;
appConfig = {
...DEFAULT_APP_CONFIG,
logLevel:
(parsedConfig.logLevel.toUpperCase() as LogLevelType) || DEFAULT_APP_CONFIG.logLevel.toUpperCase(),
model: parsedConfig.model || DEFAULT_APP_CONFIG.model,
(appConfigData.parsed.logLevel?.toUpperCase() as LogLevelType) || DEFAULT_APP_CONFIG.logLevel,
model: appConfigData.parsed.model || DEFAULT_APP_CONFIG.model,
};
} else {
writeConfigFile(APP_CONFIG_FILE_PATH, DEFAULT_APP_CONFIG);
// Create default config with comments
const defaultAppConfigContent = `# AI Console Agent Application Configuration
# Log level (DEBUG, INFO, WARN, ERROR)
logLevel=${DEFAULT_APP_CONFIG.logLevel}
# AI model to use for processing
model=${DEFAULT_APP_CONFIG.model}
# Context limit multiplier for token management
contextLimitMultiplier=${DEFAULT_APP_CONFIG.contextLimitMultiplier}`;

fs.writeFileSync(APP_CONFIG_FILE_PATH, defaultAppConfigContent);
rawAppConfig = defaultAppConfigContent;
}

if (fs.existsSync(USER_PREFS_FILE_PATH)) {
const parsedPrefs = parseConfigFile(USER_PREFS_FILE_PATH);
const userPrefsData = parseConfigFile(USER_PREFS_FILE_PATH);
rawUserConfig = userPrefsData.raw;
userPrefs = {
...DEFAULT_USER_PREFS,
techExpertiseLevel: parseInt(parsedPrefs.techExpertiseLevel) || DEFAULT_USER_PREFS.techExpertiseLevel,
preferredName: parsedPrefs.preferredName,
techExpertiseLevel:
parseInt(userPrefsData.parsed.techExpertiseLevel) || DEFAULT_USER_PREFS.techExpertiseLevel,
preferredName: userPrefsData.parsed.preferredName,
};
} else {
writeConfigFile(USER_PREFS_FILE_PATH, DEFAULT_USER_PREFS);
// Create default user preferences with comments
const defaultUserPrefsContent = `# User Preferences Configuration
# Technical expertise level (0.0 - 1.0)
techExpertiseLevel=${DEFAULT_USER_PREFS.techExpertiseLevel}
# Preferred name for interactions
preferredName=`;

fs.writeFileSync(USER_PREFS_FILE_PATH, defaultUserPrefsContent);
rawUserConfig = defaultUserPrefsContent;
}

// initGitRepo();
} catch (error) {
console.error("Error loading config:", error);
// If there's an error, use default configs
}

return { appConfig, userPrefs };
return {
appConfig,
userPrefs,
rawConfigs: {
app: rawAppConfig,
user: rawUserConfig,
},
};
}

export function saveConfig(appConfig: Partial<AppConfig>, userPrefs: Partial<UserPreferences>): boolean {
Expand All @@ -145,14 +220,7 @@ export function saveConfig(appConfig: Partial<AppConfig>, userPrefs: Partial<Use
try {
writeConfigFile(APP_CONFIG_FILE_PATH, newAppConfig);
writeConfigFile(USER_PREFS_FILE_PATH, newUserPrefs);

// if (commitChanges("Update config and preferences")) {
return true;
// } else {
// console.warn("Failed to commit changes. Rolling back...");
// revertLastChange();
// return false;
// }
} catch (error) {
console.error("Error saving config:", error);
return false;
Expand Down
12 changes: 11 additions & 1 deletion src/utils/runShellCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,17 @@ export async function runShellCommand(
stdout: stdout.toString().trim(),
stderr: stderr.toString().trim(),
};
} catch (error) {}
} catch (error) {
return {
stdout: "",
stderr: error.stderr?.toString().trim() || "",
error: {
code: error.code || 1,
message: error.message || "Unknown error occurred",
signal: error.signal,
},
};
}
}

const timestamp = Date.now();
Expand Down

0 comments on commit bc2c69a

Please sign in to comment.