mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
Restructure the AI workflow from a single-agent model to a specialized multi-agent system with distinct roles: - Add AgentType enum (Main, Orchestration, Planning, Execution) to define agent specializations - Replace AIControllerService and AIFunctionService with modular agent classes in Services/AIServices/: - MainAgent: Handles user interaction and delegates to orchestration - OrchestrationAgent: Coordinates plan execution and step-by-step workflow - PlanningAgent: Creates and revises execution plans - ExecutionAgent: Executes individual plan steps - AIController: Base class providing common agent loop functionality - Create specialized prompts (OrchestrationPrompt, ExecutionPrompt) for agent-specific behavior - Add OrchestrationResult type to communicate workflow control decisions (continue, abort, replan) - Introduce agent-specific function scoping: - ExecuteWorkflow for main agent - CompleteTask for execution agent - CompleteStep/Replan/CancelPlan for orchestration agent - SubmitPlan/AskUserQuestionPlanning for planning agent - Update BaseAIClass to use agentType property instead of isPlanningAgent boolean for model selection - Simplify ExecutionPlan and ExecutionStep types by removing execution state tracking (moved to agent coordination) - Remove PlanningEnabledAppendix and ExecutionStatus enum (superseded by agent architecture) - Add comprehensive integration tests for agent workflows This architecture provides better separation of concerns, clearer agent responsibilities, and more robust plan execution with explicit orchestration control flow.
240 lines
No EOL
7.7 KiB
Svelte
240 lines
No EOL
7.7 KiB
Svelte
<script lang="ts">
|
|
import { Resolve } from "Services/DependencyService";
|
|
import { Services } from "Services/Services";
|
|
import ChatArea from "./ChatArea.svelte";
|
|
import ChatInput from "./ChatInput.svelte";
|
|
import { tick, onMount } from "svelte";
|
|
import { conversationStore } from "../Stores/ConversationStore";
|
|
import { Conversation } from "Conversations/Conversation";
|
|
import type VaultkeeperAIPlugin from "main";
|
|
import { openPluginSettings } from "Helpers/Helpers";
|
|
import { Selector } from "Enums/Selector";
|
|
import type { WorkSpaceService } from "Services/WorkSpaceService";
|
|
import type { ChatService } from "Services/ChatService";
|
|
import type { ConversationFileSystemService } from "Services/ConversationFileSystemService";
|
|
import type { SettingsService } from "Services/SettingsService";
|
|
import { Copy } from "Enums/Copy";
|
|
import { AbortService } from "Services/AbortService";
|
|
import type { Attachment } from "Conversations/Attachment";
|
|
import ChatPlanArea from "./ChatPlanArea.svelte";
|
|
import type { ExecutionPlanStore } from "Stores/ExecutionPlanStore";
|
|
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
|
|
import { HTMLService } from "Services/HTMLService";
|
|
|
|
const plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
|
const executionPlanStore: ExecutionPlanStore = Resolve<ExecutionPlanStore>(Services.ExecutionPlanStore);
|
|
const settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
|
|
const chatService: ChatService = Resolve<ChatService>(Services.ChatService);
|
|
const workSpaceService: WorkSpaceService = Resolve<WorkSpaceService>(Services.WorkSpaceService);
|
|
const conversationService: ConversationFileSystemService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
|
|
const streamingMarkdownService: StreamingMarkdownService = Resolve<StreamingMarkdownService>(Services.StreamingMarkdownService);
|
|
const htmlService: HTMLService = Resolve<HTMLService>(Services.HTMLService);
|
|
const abortService: AbortService = Resolve<AbortService>(Services.AbortService);
|
|
|
|
let chatContainer: HTMLDivElement;
|
|
let chatArea: ChatArea;
|
|
let chatInput: ChatInput;
|
|
|
|
let hasNoApiKey = false;
|
|
let isSubmitting = false;
|
|
let busyPlanning = false;
|
|
let editModeActive = false;
|
|
let planningModeActive = false;
|
|
let currentStreamingMessageId: string | null = null;
|
|
|
|
let conversation: Conversation = new Conversation();
|
|
let attachments: Attachment[] = [];
|
|
|
|
let currentThought: string | null = null;
|
|
|
|
export function focusInput() {
|
|
chatInput?.focusInput();
|
|
}
|
|
|
|
export function resetChatArea() {
|
|
chatArea.resetChatArea();
|
|
}
|
|
|
|
onMount(() => {
|
|
if (chatContainer) {
|
|
plugin.registerDomEvent(chatContainer, 'click', handleLinkClick);
|
|
}
|
|
});
|
|
|
|
async function handleLinkClick(evt: MouseEvent) {
|
|
const target = evt.target as HTMLElement;
|
|
|
|
const link = target.closest(`.${Selector.MarkDownLink}`) as HTMLAnchorElement | null;
|
|
if (!link) {
|
|
return;
|
|
}
|
|
|
|
const href = link.getAttribute('href');
|
|
if (!href || !href.startsWith('#/page/')) {
|
|
return;
|
|
}
|
|
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
|
|
const encodedPath = href.replace('#/page/', '');
|
|
const notePath = decodeURIComponent(encodedPath);
|
|
await workSpaceService.openNote(notePath);
|
|
}
|
|
|
|
function handleNoApiKey(): boolean {
|
|
hasNoApiKey = settingsService.getApiKeyForCurrentModel().trim() == "";
|
|
if (hasNoApiKey) {
|
|
openPluginSettings(plugin);
|
|
}
|
|
return hasNoApiKey;
|
|
}
|
|
|
|
function handleStop() {
|
|
chatService.stop();
|
|
currentThought = null;
|
|
}
|
|
|
|
async function handleSubmit(userRequest: string, formattedRequest: string) {
|
|
if (handleNoApiKey()) {
|
|
return;
|
|
}
|
|
|
|
const currentRequest = userRequest;
|
|
|
|
await chatService.submit(conversation, editModeActive, planningModeActive, currentRequest, formattedRequest, attachments, {
|
|
onSubmit: () => {
|
|
isSubmitting = true;
|
|
attachments = [];
|
|
},
|
|
onStreamingUpdate: (streamingId) => {
|
|
conversation = conversation;
|
|
currentStreamingMessageId = streamingId;
|
|
chatArea.updateChatAreaLayout("smooth");
|
|
},
|
|
onThoughtUpdate: (thought) => {
|
|
if (thought !== Copy.AIThoughtMessage) {
|
|
currentThought = thought;
|
|
} else if (currentThought !== null) {
|
|
// we are in-between thoughts so use generic copy
|
|
currentThought = thought;
|
|
}
|
|
},
|
|
onPlanningStarted: () => {
|
|
busyPlanning = true;
|
|
},
|
|
onPlanningFinished: () => {
|
|
busyPlanning = false;
|
|
},
|
|
onUserQuestion: async (question) => {
|
|
const displayEl = createEl("div");
|
|
const formattedHtml = streamingMarkdownService.formatText(question);
|
|
htmlService.setHTMLContent(displayEl, formattedHtml);
|
|
chatInput.setDisplayItem(displayEl);
|
|
return new Promise<string>((resolve) => {
|
|
chatInput.enterQuestionMode(resolve);
|
|
});
|
|
},
|
|
onPlanUpdate: (executionPlan) => {
|
|
executionPlanStore.setPlan(executionPlan);
|
|
},
|
|
onPlanStepUpdate: (currentStepIndex) => {
|
|
executionPlanStore.setCurrentStepIndex(currentStepIndex);
|
|
},
|
|
onPlanReset: () => {
|
|
executionPlanStore.clearPlan();
|
|
},
|
|
onComplete: async () => {
|
|
isSubmitting = false;
|
|
busyPlanning = false;
|
|
currentThought = null;
|
|
executionPlanStore.clearPlan();
|
|
chatInput.clearDisplayItem();
|
|
abortService.reset();
|
|
tick().then(() => {
|
|
chatArea.updateChatAreaLayout("smooth", true);
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
$: if ($conversationStore.shouldReset) {
|
|
conversation = new Conversation();
|
|
conversationService.resetCurrentConversation();
|
|
|
|
isSubmitting = false;
|
|
currentStreamingMessageId = null;
|
|
currentThought = null;
|
|
|
|
conversationStore.clearResetFlag();
|
|
}
|
|
|
|
$: if ($conversationStore.conversationToLoad) {
|
|
conversation.contents = [];
|
|
|
|
isSubmitting = false;
|
|
currentStreamingMessageId = null;
|
|
currentThought = null;
|
|
|
|
chatArea.resetChatArea();
|
|
|
|
tick().then(() => {
|
|
if ($conversationStore.conversationToLoad) {
|
|
const { conversation: loadedConversation, filePath } = $conversationStore.conversationToLoad;
|
|
conversation = loadedConversation;
|
|
conversationService.setCurrentConversationPath(filePath);
|
|
chatService.onNameChanged?.(loadedConversation.title);
|
|
conversationStore.clearLoadFlag();
|
|
chatArea.updateChatAreaLayout("instant", true);
|
|
}
|
|
});
|
|
}
|
|
|
|
$: if ($conversationStore.shouldDeactivateEditMode) {
|
|
conversationStore.clearEditModeFlag();
|
|
planningModeActive = false;
|
|
editModeActive = false;
|
|
}
|
|
</script>
|
|
|
|
<main class="container">
|
|
<ChatPlanArea executionPlanState={executionPlanStore.executionPlanState} {editModeActive} {busyPlanning}/>
|
|
|
|
<div id="chat-container">
|
|
<ChatArea messages={conversation.contents} bind:this={chatArea} bind:currentThought bind:isSubmitting bind:chatContainer
|
|
currentStreamingMessageId={currentStreamingMessageId} editModeActive={editModeActive}/>
|
|
</div>
|
|
|
|
<ChatInput
|
|
bind:this={chatInput}
|
|
bind:attachments
|
|
bind:editModeActive
|
|
bind:planningModeActive
|
|
{hasNoApiKey}
|
|
{isSubmitting}
|
|
onSubmit={handleSubmit}
|
|
onStop={handleStop}
|
|
/>
|
|
</main>
|
|
|
|
<style>
|
|
.container {
|
|
display: grid;
|
|
grid-template-rows: auto 1fr auto var(--size-2-1);
|
|
grid-template-columns: 1fr;
|
|
height: calc(100% - var(--size-4-16));
|
|
border-radius: var(--radius-m);
|
|
color: var(--font-interface-theme);
|
|
}
|
|
|
|
#chat-container {
|
|
height: 100%;
|
|
width: 100%;
|
|
max-width: 1000px;
|
|
justify-self: center;
|
|
user-select: text;
|
|
grid-row: 2;
|
|
grid-column: 1;
|
|
overflow: hidden;
|
|
}
|
|
</style> |