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

Feat/chat-run-command #55

Merged
merged 4 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions packages/qllm-cli/src/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { ConfigManager } from "./config-manager";
import { ioManager } from "../utils/io-manager";
import ImageManager from "./image-manager";

declare var process: NodeJS.Process; //eslint-disable-line

export class Chat {
private conversationManager: ConversationManager;
private conversationId: string | null = null;
Expand Down Expand Up @@ -72,6 +74,10 @@ export class Chat {
process.exit(0);
}

if (input.trim() === "") {
return;
}

if (input.startsWith("/")) {
await this.handleSpecialCommand(input);
} else {
Expand Down
135 changes: 134 additions & 1 deletion packages/qllm-cli/src/chat/command-processor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// packages/qllm-cli/src/chat/command-processor.ts
import { ConversationManager } from "qllm-lib";
import { ConversationManager, TextContent } from "qllm-lib";
import { ChatConfig } from "./chat-config";
import { ConfigManager } from "./config-manager";
import { IOManager } from "../utils/io-manager";
Expand All @@ -10,6 +10,10 @@ import { displayCurrentOptions } from "./commands/display-current-options";
import { displayConversation } from "./commands/display-conversation";
import { listModels } from "./commands/list-models";
import { listProviders } from "./commands/list-providers";
import { runActionCommand } from "../commands/run-command";
import { fileExists, writeToFile } from "../utils/write-file";

declare var process: NodeJS.Process; // eslint-disable-line no-var

export interface CommandContext {
config: ChatConfig;
Expand Down Expand Up @@ -45,6 +49,8 @@ export class CommandProcessor {
select: this.selectConversation,
delete: this.deleteConversation,
deleteall: this.deleteAllConversations,
run: this.runTemplate,
save: this.saveResponse,
};

async processCommand(
Expand All @@ -64,6 +70,58 @@ export class CommandProcessor {
process.exit(0);
}

private async runTemplate(
args: string[],
{
conversationManager,
ioManager,
conversationId,
config,
}: CommandContext,
): Promise<void> {
if (!conversationId) {
ioManager.displayError(
"No active conversation. Please start a chat first.",
);
return;
}

const templateUrl = args[0];
if (!templateUrl) {
ioManager.displayError(
"Please provide a template URL or local file path.",
);
return;
}
const result = await runActionCommand(templateUrl, {
model: config.get("model"),
provider: config.get("provider"),
maxTokens: config.get("maxTokens"),
temperature: config.get("temperature"),
stream: true,
});

if (result && conversationId) {
conversationManager.addMessage(conversationId, {
role: "user",
content: {
type: "text",
text: result.question,
},
providerId: "template",
});

conversationManager.addMessage(conversationId, {
role: "assistant",
content: {
type: "text",
text: result.response,
},
providerId: "template",
});
}
}

private async setModel(
args: string[],
{ configManager, ioManager }: CommandContext,
Expand Down Expand Up @@ -266,4 +324,79 @@ export class CommandProcessor {
await conversationManager.deleteAllConversations();
ioManager.displaySuccess("All conversations deleted.");
}

private async saveResponse(
args: string[],
{ conversationManager, ioManager, conversationId }: CommandContext,
): Promise<void> {
if (!conversationId) {
ioManager.displayError("No active conversation.");
return;
}

const filePath = args[0];
if (!filePath) {
ioManager.displayError("Please provide a file path.");
return;
}

const conversation =
await conversationManager.getConversation(conversationId);
if (
!conversation ||
!conversation.messages ||
conversation.messages.length === 0
) {
ioManager.displayError("No messages found in the conversation.");
return;
}

// Prepare the full conversation content
const conversationContent = conversation.messages
.map((msg) => {
const role = msg.role === "assistant" ? "Assistant" : "User";
const text = Array.isArray(msg.content)
? msg.content
.filter(
(c): c is TextContent =>
"type" in c && c.type === "text",
)
.map((c) => c.text)
.join("\n")
: "type" in msg.content && msg.content.type === "text"
? msg.content.text
: "";

// Format each message with a timestamp and role
const timestamp = new Date(msg.timestamp).toLocaleString(); // Assuming msg has a timestamp property
return `[${timestamp}] ${role}: ${text}`;
})
.join("\n\n---\n\n"); // Delimiter between messages

const isFileExists = await fileExists(filePath);

if (isFileExists) {
const confirm = await ioManager.confirmAction(
`File ${filePath} already exists. Do you to continue and overwrite it?`,
);

if (!confirm) {
ioManager.displayWarning(`Operation cancelled by user.`);
return;
}
}

await writeToFile(filePath, conversationContent, {
flag: "w",
});

const startOfContent =
conversationContent.length > 20
? conversationContent.substring(0, 20) + "..."
: conversationContent;

ioManager.displaySuccess(
`Full conversation ${startOfContent} saved to ${filePath}`,
);
}
}
24 changes: 23 additions & 1 deletion packages/qllm-cli/src/chat/commands/show-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const helpGroups = [
command: "/deleteall",
description: "Delete all past conversations",
},
{
command: "/run <template>",
description: "Run a predefined prompt template <template> can be an URL or a local file",
},
{
command: "/save <file>",
description: "Write the current conversation to a file <file> can be a local file"
}
],
},
{
Expand Down Expand Up @@ -126,7 +134,21 @@ function showCommandHelp(
ioManager.displayInfo(
` ${ioManager.colorize(commandHelp.command, "cyan")}`,
);
// Add more detailed usage information here if available

// Add more detailed usage information for specific commands
if (command === "run") {
ioManager.newLine();
ioManager.displayInfo("The run command allows you to execute predefined chat templates.");
ioManager.displayInfo("Templates are predefined conversation starters or workflows.");
ioManager.displayInfo("Example:");
ioManager.displayInfo(" /run code-review");
ioManager.newLine();
ioManager.displayInfo("Available templates:");
ioManager.displayInfo(" - code-review: Start a code review session");
ioManager.displayInfo(" - brainstorm: Begin a brainstorming session");
ioManager.displayInfo(" - debug: Initiate a debugging session");
// Add more templates as they become available
}
} else {
ioManager.displayError(`No help available for command: ${command}`);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/qllm-cli/src/commands/chat-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ export const chatAction = async (options: ChatCommandOptions) => {
await chat.start();
} catch (error) {
ioManager.displayError("An error occurred while starting the chat:");
console.error(error);
if (error instanceof Error) {
ioManager.displayError(error.message);
} else {
ioManager.displayError("An unknown error occurred");
}
}
};

Expand Down
14 changes: 12 additions & 2 deletions packages/qllm-cli/src/commands/run-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// packages/qllm-cli/src/commands/run-command.ts

import { Command } from "commander";
import {
RunCommandOptions,
RunCommandOptionsSchema,
Expand All @@ -19,7 +18,7 @@ declare var process: NodeJS.Process; //eslint-disable-line
export const runActionCommand = async (
templateSource: string,
options: Partial<RunCommandOptions>,
) => {
): Promise<{ question: string; response: string } | undefined> => {
const ioManager = new IOManager();
const cliConfig = CliConfigManager.getInstance();

Expand Down Expand Up @@ -59,6 +58,12 @@ export const runActionCommand = async (
const executor = setupExecutor(ioManager, spinner);
const provider = await getLLMProvider(providerName);

let question = "";

executor.on("contentPrepared", (content: string) => {
question = content;
});

const result = await executor.execute({
template,
variables: { ...variables },
Expand Down Expand Up @@ -91,6 +96,11 @@ export const runActionCommand = async (
});

await handleOutput(result, validOptions, ioManager);

return {
question: question,
response: result.response,
};
} catch (error) {
spinner.error({
text: `Error executing template: ${(error as Error).message}`,
Expand Down
11 changes: 11 additions & 0 deletions packages/qllm-cli/src/utils/io-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getBorderCharacters, table } from "table";
import { createSpinner } from "nanospinner";
import { Table } from "console-table-printer";
import prompts from "prompts";
import { writeToFile } from "../write-file";

const stdout = {
log: (...args: any[]) => {
Expand Down Expand Up @@ -388,6 +389,16 @@ export class IOManager {
return undefined;
}
}

async promptYesNo(message: string): Promise<boolean> {
const response = await prompts({
type: 'confirm',
name: 'value',
message: message,
initial: true, // Default to 'yes'
});
return response.value; // Returns true for 'yes', false for 'no'
}
}

// Create a singleton instance for easy access
Expand Down
20 changes: 16 additions & 4 deletions packages/qllm-cli/src/utils/write-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export async function writeToFile(
encoding?: BufferEncoding;
mode?: number;
flag?: string;
append?: boolean;
} = {},
): Promise<void> {
if (typeof filePath !== "string" || filePath.trim().length === 0) {
Expand All @@ -18,7 +19,8 @@ export async function writeToFile(
throw new Error("Content must be a string");
}

const { encoding = "utf8", mode = 0o666, flag = "w" } = options;
const { encoding = "utf8", mode = 0o666, append = false } = options;
const flag = append ? "a" : "w";

let fileHandle: fs.FileHandle | null = null;
try {
Expand All @@ -27,13 +29,23 @@ export async function writeToFile(

fileHandle = await fs.open(filePath, flag, mode);
await fileHandle.writeFile(content, { encoding });
} catch (error) {
throw error;
} finally {
if (fileHandle) {
try {
await fileHandle.close();
} catch (closeError) {}
} catch (closeError) {
// Handle close error if needed
}
}
}
}


export async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true; // File exists
} catch {
return false; // File does not exist
}
}
Loading