mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
Implement comprehensive artifact tracking to record all file modifications made by the AI agent during conversations. Add artifact persistence, garbage collection, and UI updates with new "Discuss" button styling.
152 lines
5.4 KiB
TypeScript
152 lines
5.4 KiB
TypeScript
import { Semaphore } from "Helpers/Semaphore";
|
|
import { Resolve } from "./DependencyService";
|
|
import { Services } from "./Services";
|
|
import type { ConversationFileSystemService } from "./ConversationFileSystemService";
|
|
import type { ConversationNamingService } from "./ConversationNamingService";
|
|
import { Conversation } from "Conversations/Conversation";
|
|
import { ConversationContent } from "Conversations/ConversationContent";
|
|
import { Role } from "Enums/Role";
|
|
import { Notice } from "obsidian";
|
|
import type { EventService } from "./EventService";
|
|
import { Event } from "Enums/Event";
|
|
import { AbortService } from "./AbortService";
|
|
import { Exception } from "Helpers/Exception";
|
|
import type { Attachment } from "Conversations/Attachment";
|
|
import { Reference } from "Conversations/Reference";
|
|
import type { WorkSpaceService } from "./WorkSpaceService";
|
|
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
|
import type { MainAgent } from "./AIServices/MainAgent";
|
|
import type { ChatMode } from "Enums/ChatMode";
|
|
import type { PlanApprovalResponse } from "Types/PlanApprovalResponse";
|
|
import type { Artifact } from "Conversations/Artifact";
|
|
|
|
export interface IChatServiceCallbacks {
|
|
onSubmit: () => void;
|
|
onStreamingUpdate: () => void;
|
|
onThoughtUpdate: (thought: string | null) => void;
|
|
onToolCallStarted: (toolName: string) => void;
|
|
onArtifactProduced: (artifact: Artifact) => void;
|
|
onPlanningStarted: () => void;
|
|
onPlanningFinished: () => void;
|
|
onUserQuestion: (question: string) => Promise<string>;
|
|
onPlanApprovalRequest: (plan: ExecutionPlan) => Promise<PlanApprovalResponse>
|
|
onPlanUpdate: (executionPlan: ExecutionPlan) => void;
|
|
onPlanStepUpdate: (currentStepIndex: number) => void;
|
|
onPlanReset: () => void;
|
|
onComplete: () => void;
|
|
}
|
|
|
|
export class ChatService {
|
|
|
|
private mainAgent: MainAgent;
|
|
private conversationService: ConversationFileSystemService;
|
|
private namingService: ConversationNamingService;
|
|
private workSpaceService: WorkSpaceService;
|
|
private eventService: EventService;
|
|
private abortService: AbortService;
|
|
|
|
private semaphore: Semaphore;
|
|
private semaphoreHeld: boolean = false;
|
|
|
|
constructor() {
|
|
this.mainAgent = Resolve<MainAgent>(Services.MainAgent);
|
|
this.conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
|
|
this.namingService = Resolve<ConversationNamingService>(Services.ConversationNamingService);
|
|
this.workSpaceService = Resolve<WorkSpaceService>(Services.WorkSpaceService);
|
|
this.eventService = Resolve<EventService>(Services.EventService);
|
|
this.abortService = Resolve<AbortService>(Services.AbortService);
|
|
this.semaphore = new Semaphore(1, false);
|
|
|
|
this.mainAgent.setSaveCallback(async (conversation) => {
|
|
await this.saveConversation(conversation);
|
|
});
|
|
}
|
|
|
|
public onNameChanged: ((name: string) => void) | undefined = undefined;
|
|
|
|
public async submit(conversation: Conversation, chatMode: ChatMode, userRequest: string, formattedRequest: string, attachments: Attachment[], callbacks: IChatServiceCallbacks) {
|
|
if (!await this.semaphore.wait()) {
|
|
return;
|
|
}
|
|
|
|
this.semaphoreHeld = true;
|
|
|
|
try {
|
|
if (userRequest.trim() === "") {
|
|
return;
|
|
}
|
|
|
|
this.abortService.initialiseAbortController();
|
|
|
|
await this.abortService.abortableOperation(async () => {
|
|
const firstMessage = conversation.contents.length === 0;
|
|
|
|
const conversationContent = new ConversationContent({
|
|
role: Role.User,
|
|
content: this.requestWithContext(formattedRequest),
|
|
displayContent: userRequest
|
|
});
|
|
conversation.contents.push(conversationContent);
|
|
|
|
await this.saveConversation(conversation);
|
|
callbacks.onStreamingUpdate();
|
|
|
|
let namingPromise: Promise<void> | undefined;
|
|
if (firstMessage) {
|
|
this.onNameChanged?.(conversation.title); // on change for initial conversation name
|
|
namingPromise = this.namingService.requestName(conversation, formattedRequest, this.onNameChanged);
|
|
}
|
|
|
|
if (attachments.length > 0) {
|
|
// Add any attachments that came from paste / drop
|
|
conversation.contents.push(new ConversationContent({
|
|
role: Role.User,
|
|
attachments: attachments,
|
|
shouldDisplayContent: false
|
|
}));
|
|
|
|
conversationContent.references = attachments.map(attachment =>
|
|
new Reference(attachment.fileName, attachment.approximateFileSizeMB()));
|
|
}
|
|
await this.saveConversation(conversation);
|
|
|
|
callbacks.onSubmit();
|
|
callbacks.onStreamingUpdate();
|
|
|
|
await this.mainAgent.runMainAgent(conversation, chatMode, callbacks);
|
|
|
|
if (namingPromise) {
|
|
const timeout = new Promise<void>(resolve => window.setTimeout(resolve, 5000));
|
|
await Promise.race([namingPromise, timeout]);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
if (!AbortService.isAbortError(error)) {
|
|
Exception.log(error);
|
|
new Notice("Vaultkeeper AI encountered an error");
|
|
}
|
|
} finally {
|
|
this.eventService.trigger(Event.DiffClosed);
|
|
callbacks.onThoughtUpdate(null);
|
|
callbacks.onComplete();
|
|
if (this.semaphoreHeld) {
|
|
this.semaphoreHeld = false;
|
|
this.semaphore.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
public stop() {
|
|
this.abortService.abort("User requested cancellation");
|
|
this.eventService.trigger(Event.DiffClosed);
|
|
}
|
|
|
|
private requestWithContext(request: string) {
|
|
const activeFile = this.workSpaceService.getActiveFile();
|
|
return activeFile ? `${request}\nUser current active file: "${activeFile.path}"` : request;
|
|
}
|
|
|
|
private async saveConversation(conversation: Conversation) {
|
|
await this.conversationService.saveConversation(conversation);
|
|
}
|
|
}
|