* feat: move necessary files to jan root Signed-off-by: James <james@jan.ai> * chore: check model dir --------- Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai> Co-authored-by: Louis <louis@jan.ai>
215 lines
6.9 KiB
TypeScript
215 lines
6.9 KiB
TypeScript
import { PluginType, fs } from "@janhq/core";
|
|
import { ConversationalPlugin } from "@janhq/core/lib/plugins";
|
|
import { Message, Conversation } from "@janhq/core/lib/types";
|
|
|
|
/**
|
|
* JanConversationalPlugin is a ConversationalPlugin implementation that provides
|
|
* functionality for managing conversations.
|
|
*/
|
|
export default class JanConversationalPlugin implements ConversationalPlugin {
|
|
/**
|
|
* Returns the type of the plugin.
|
|
*/
|
|
type(): PluginType {
|
|
return PluginType.Conversational;
|
|
}
|
|
|
|
/**
|
|
* Called when the plugin is loaded.
|
|
*/
|
|
onLoad() {
|
|
console.debug("JanConversationalPlugin loaded");
|
|
fs.mkdir("conversations");
|
|
}
|
|
|
|
/**
|
|
* Called when the plugin is unloaded.
|
|
*/
|
|
onUnload() {
|
|
console.debug("JanConversationalPlugin unloaded");
|
|
}
|
|
|
|
/**
|
|
* Returns a Promise that resolves to an array of Conversation objects.
|
|
*/
|
|
getConversations(): Promise<Conversation[]> {
|
|
return this.getConversationDocs().then((conversationIds) =>
|
|
Promise.all(
|
|
conversationIds.map((conversationId) =>
|
|
this.loadConversationFromMarkdownFile(
|
|
`conversations/${conversationId}/${conversationId}.md`
|
|
)
|
|
)
|
|
).then((conversations) =>
|
|
conversations.sort(
|
|
(a, b) =>
|
|
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Saves a Conversation object to a Markdown file.
|
|
* @param conversation The Conversation object to save.
|
|
*/
|
|
saveConversation(conversation: Conversation): Promise<void> {
|
|
return this.writeMarkdownToFile(conversation);
|
|
}
|
|
|
|
/**
|
|
* Deletes a conversation with the specified ID.
|
|
* @param conversationId The ID of the conversation to delete.
|
|
*/
|
|
deleteConversation(conversationId: string): Promise<void> {
|
|
return fs.rmdir(`conversations/${conversationId}`);
|
|
}
|
|
|
|
/**
|
|
* Returns a Promise that resolves to an array of conversation IDs.
|
|
* The conversation IDs are the names of the Markdown files in the "conversations" directory.
|
|
* @private
|
|
*/
|
|
private async getConversationDocs(): Promise<string[]> {
|
|
return fs.listFiles("conversations").then((files: string[]) => {
|
|
return Promise.all(files.filter((file) => file.startsWith("jan-")));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parses a Markdown string and returns a Conversation object.
|
|
* @param markdown The Markdown string to parse.
|
|
* @private
|
|
*/
|
|
private parseConversationMarkdown(markdown: string): Conversation {
|
|
const conversation: Conversation = {
|
|
id: "",
|
|
name: "",
|
|
messages: [],
|
|
};
|
|
var currentMessage: Message | undefined = undefined;
|
|
for (const line of markdown.split("\n")) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith("- id:")) {
|
|
conversation.id = trimmedLine.replace("- id:", "").trim();
|
|
} else if (trimmedLine.startsWith("- modelId:")) {
|
|
conversation.modelId = trimmedLine.replace("- modelId:", "").trim();
|
|
} else if (trimmedLine.startsWith("- name:")) {
|
|
conversation.name = trimmedLine.replace("- name:", "").trim();
|
|
} else if (trimmedLine.startsWith("- lastMessage:")) {
|
|
conversation.message = trimmedLine.replace("- lastMessage:", "").trim();
|
|
} else if (trimmedLine.startsWith("- summary:")) {
|
|
conversation.summary = trimmedLine.replace("- summary:", "").trim();
|
|
} else if (
|
|
trimmedLine.startsWith("- createdAt:") &&
|
|
currentMessage === undefined
|
|
) {
|
|
conversation.createdAt = trimmedLine.replace("- createdAt:", "").trim();
|
|
} else if (trimmedLine.startsWith("- updatedAt:")) {
|
|
conversation.updatedAt = trimmedLine.replace("- updatedAt:", "").trim();
|
|
} else if (trimmedLine.startsWith("- botId:")) {
|
|
conversation.botId = trimmedLine.replace("- botId:", "").trim();
|
|
} else if (trimmedLine.startsWith("- user:")) {
|
|
if (currentMessage)
|
|
currentMessage.user = trimmedLine.replace("- user:", "").trim();
|
|
} else if (trimmedLine.startsWith("- createdAt:")) {
|
|
if (currentMessage)
|
|
currentMessage.createdAt = trimmedLine
|
|
.replace("- createdAt:", "")
|
|
.trim();
|
|
|
|
currentMessage.updatedAt = currentMessage.createdAt;
|
|
} else if (trimmedLine.startsWith("- message:")) {
|
|
if (currentMessage)
|
|
currentMessage.message = trimmedLine.replace("- message:", "").trim();
|
|
} else if (trimmedLine.startsWith("- Message ")) {
|
|
const messageMatch = trimmedLine.match(/- Message (m-\d+):/);
|
|
if (messageMatch) {
|
|
if (currentMessage) {
|
|
conversation.messages.push(currentMessage);
|
|
}
|
|
currentMessage = { id: messageMatch[1] };
|
|
}
|
|
} else if (
|
|
currentMessage?.message &&
|
|
!trimmedLine.startsWith("## Messages")
|
|
) {
|
|
currentMessage.message = currentMessage.message + "\n" + line.trim();
|
|
} else if (trimmedLine.startsWith("## Messages")) {
|
|
currentMessage = undefined;
|
|
}
|
|
}
|
|
|
|
if (currentMessage) {
|
|
conversation.messages.push(currentMessage);
|
|
}
|
|
return conversation;
|
|
}
|
|
|
|
/**
|
|
* Loads a Conversation object from a Markdown file.
|
|
* @param filePath The path to the Markdown file.
|
|
* @private
|
|
*/
|
|
private async loadConversationFromMarkdownFile(
|
|
filePath: string
|
|
): Promise<Conversation | undefined> {
|
|
try {
|
|
const markdown: string = await fs.readFile(filePath);
|
|
return this.parseConversationMarkdown(markdown);
|
|
} catch (err) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a Markdown string from a Conversation object.
|
|
* @param conversation The Conversation object to generate Markdown from.
|
|
* @private
|
|
*/
|
|
private generateMarkdown(conversation: Conversation): string {
|
|
// Generate the Markdown content based on the Conversation object
|
|
const conversationMetadata = `
|
|
- id: ${conversation.id}
|
|
- modelId: ${conversation.modelId}
|
|
- name: ${conversation.name}
|
|
- lastMessage: ${conversation.message}
|
|
- summary: ${conversation.summary}
|
|
- createdAt: ${conversation.createdAt}
|
|
- updatedAt: ${conversation.updatedAt}
|
|
- botId: ${conversation.botId}
|
|
`;
|
|
|
|
const messages = conversation.messages.map(
|
|
(message) => `
|
|
- Message ${message.id}:
|
|
- createdAt: ${message.createdAt}
|
|
- user: ${message.user}
|
|
- message: ${message.message?.trim()}
|
|
`
|
|
);
|
|
|
|
return `## Conversation Metadata
|
|
${conversationMetadata}
|
|
## Messages
|
|
${messages.map((msg) => msg.trim()).join("\n")}
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Writes a Conversation object to a Markdown file.
|
|
* @param conversation The Conversation object to write to a Markdown file.
|
|
* @private
|
|
*/
|
|
private async writeMarkdownToFile(conversation: Conversation) {
|
|
// Generate the Markdown content
|
|
const markdownContent = this.generateMarkdown(conversation);
|
|
await fs.mkdir(`conversations/${conversation.id}`);
|
|
// Write the content to a Markdown file
|
|
await fs.writeFile(
|
|
`conversations/${conversation.id}/${conversation.id}.md`,
|
|
markdownContent
|
|
);
|
|
}
|
|
}
|