mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: rename toolDefinitions to aiFunctionDefinitions and add AIFunctionUsageMode support
- Rename toolDefinitions to aiFunctionDefinitions across all AI classes - Add aiFunctionUsageMode property to control function calling behavior - Implement provider-specific tool_choice/tool_config based on usage mode - Remove AIController and create BaseAgent base class - Update ExecutionPrompt with scope of execution guidelines - Simplify ChatArea layout update logic by removing debounce - Add naming service completion wait in ChatService - Replace console.warn with Exception.warn in InputService - Delete unused AIController.ts file - Update all tests to use new aiFunctionDefinitions property
This commit is contained in:
parent
9276d2d0cd
commit
ac835d1346
19 changed files with 216 additions and 90 deletions
|
|
@ -15,6 +15,7 @@ import { ApiError, ApiErrorType } from "Types/ApiError";
|
|||
import type { AbortService } from "Services/AbortService";
|
||||
import type { IAIFileService } from "./IAIFileService";
|
||||
import { AgentType } from "Enums/AgentType";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export abstract class BaseAIClass implements IAIClass {
|
||||
|
||||
|
|
@ -28,7 +29,8 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
private _systemPrompt: string = "";
|
||||
private _userInstruction: string = "";
|
||||
private _agentType: AgentType = AgentType.Main;
|
||||
private _toolDefinitions: IAIFunctionDefinition[] = [];
|
||||
private _aiFunctionDefinitions: IAIFunctionDefinition[] = [];
|
||||
private _aiFunctionUsageMode: AIFunctionUsageMode = AIFunctionUsageMode.Auto;
|
||||
|
||||
protected constructor(provider: AIProvider) {
|
||||
this.provider = provider;
|
||||
|
|
@ -60,12 +62,12 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
return this._userInstruction;
|
||||
}
|
||||
|
||||
public get toolDefinitions(): IAIFunctionDefinition[] {
|
||||
return this._toolDefinitions;
|
||||
public get aiFunctionDefinitions(): IAIFunctionDefinition[] {
|
||||
return this._aiFunctionDefinitions;
|
||||
}
|
||||
|
||||
public set toolDefinitions(toolDefinitions: IAIFunctionDefinition[]) {
|
||||
this._toolDefinitions = toolDefinitions;
|
||||
public set aiFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]) {
|
||||
this._aiFunctionDefinitions = aiFunctionDefinitions;
|
||||
}
|
||||
|
||||
public get agentType() {
|
||||
|
|
@ -76,6 +78,14 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
this._agentType = agentType;
|
||||
}
|
||||
|
||||
public get aiFunctionUsageMode(): AIFunctionUsageMode {
|
||||
return this._aiFunctionUsageMode;
|
||||
}
|
||||
|
||||
public set aiFunctionUsageMode(mode: AIFunctionUsageMode) {
|
||||
this._aiFunctionUsageMode = mode;
|
||||
}
|
||||
|
||||
public abstract streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown>;
|
||||
|
||||
public abstract formatBinaryFiles(attachments: Attachment[]): string;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { isTextFile } from "Enums/FileType";
|
|||
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { ApiError, ApiErrorType } from "Types/ApiError";
|
||||
import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class Claude extends BaseAIClass {
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ export class Claude extends BaseAIClass {
|
|||
};
|
||||
|
||||
const tools: ToolUnion[] = this.addCacheControlToTools([
|
||||
webSearchTool, ...this.mapFunctionDefinitions(this.toolDefinitions)]);
|
||||
webSearchTool, ...this.mapFunctionDefinitions(this.aiFunctionDefinitions)]);
|
||||
|
||||
const requestBody = {
|
||||
model: this.model(),
|
||||
|
|
@ -76,6 +77,7 @@ export class Claude extends BaseAIClass {
|
|||
system: systemPrompt,
|
||||
messages: messages,
|
||||
tools: tools,
|
||||
tool_choice: this.buildClaudeToolChoice(),
|
||||
stream: true
|
||||
};
|
||||
|
||||
|
|
@ -365,6 +367,22 @@ export class Claude extends BaseAIClass {
|
|||
return cachedMessages;
|
||||
}
|
||||
|
||||
private buildClaudeToolChoice(): { type: string } {
|
||||
// If no tools defined, fall back to auto
|
||||
if (this.aiFunctionDefinitions.length === 0) {
|
||||
return { type: "auto" };
|
||||
}
|
||||
|
||||
switch (this.aiFunctionUsageMode) {
|
||||
case AIFunctionUsageMode.Auto:
|
||||
return { type: "auto" };
|
||||
case AIFunctionUsageMode.Enabled:
|
||||
return { type: "any" };
|
||||
case AIFunctionUsageMode.Disabled:
|
||||
return { type: "none" };
|
||||
}
|
||||
}
|
||||
|
||||
private extractRetryDelay(error: ApiError): number | undefined {
|
||||
if (error.info.type !== ApiErrorType.RATE_LIMIT || !error.info.responseHeaders) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Exception } from "Helpers/Exception";
|
|||
import { ApiError, ApiErrorType } from "Types/ApiError";
|
||||
import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import type { GeminiRetryInfo, GeminiErrorResponse } from "./GeminiTypes";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class Gemini extends BaseAIClass {
|
||||
|
||||
|
|
@ -103,11 +104,11 @@ export class Gemini extends BaseAIClass {
|
|||
information, recent events, news, or facts that may have changed.
|
||||
After calling this, you will be able to perform web searches.`,
|
||||
},
|
||||
...this.mapFunctionDefinitions(this.toolDefinitions),
|
||||
...this.mapFunctionDefinitions(this.aiFunctionDefinitions),
|
||||
]
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
const requestBody: Record<string, unknown> = {
|
||||
system_instruction: {
|
||||
parts: [
|
||||
{
|
||||
|
|
@ -137,6 +138,11 @@ export class Gemini extends BaseAIClass {
|
|||
tools: [tools]
|
||||
};
|
||||
|
||||
// Only include tool_config when actually sending function declarations (not for google_search)
|
||||
if (!requestWebSearch) {
|
||||
requestBody.tool_config = this.buildGeminiToolConfig();
|
||||
}
|
||||
|
||||
yield* this.streamingService.streamRequest(
|
||||
`${AIProviderURL.Gemini}/${this.model()}:streamGenerateContent?key=${this.apiKey}&alt=sse`,
|
||||
requestBody,
|
||||
|
|
@ -354,6 +360,22 @@ export class Gemini extends BaseAIClass {
|
|||
return JSON.stringify(parts);
|
||||
}
|
||||
|
||||
private buildGeminiToolConfig(): { function_calling_config: { mode: string } } {
|
||||
// If no tools defined, fall back to auto
|
||||
if (this.aiFunctionDefinitions.length === 0) {
|
||||
return { function_calling_config: { mode: "AUTO" } };
|
||||
}
|
||||
|
||||
switch (this.aiFunctionUsageMode) {
|
||||
case AIFunctionUsageMode.Auto:
|
||||
return { function_calling_config: { mode: "AUTO" } };
|
||||
case AIFunctionUsageMode.Enabled:
|
||||
return { function_calling_config: { mode: "ANY" } };
|
||||
case AIFunctionUsageMode.Disabled:
|
||||
return { function_calling_config: { mode: "NONE" } };
|
||||
}
|
||||
}
|
||||
|
||||
private extractRetryDelay(error: ApiError): number | undefined {
|
||||
if (error.info.type !== ApiErrorType.RATE_LIMIT || !error.info.responseBody) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ import type { Attachment } from "Conversations/Attachment";
|
|||
import type { IAIFunctionDefinition } from "./FunctionDefinitions/IAIFunctionDefinition";
|
||||
import type { AIProvider } from "Enums/ApiProvider";
|
||||
import type { AgentType } from "Enums/AgentType";
|
||||
import type { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export interface IAIClass {
|
||||
get currentProvider(): AIProvider;
|
||||
set agentType(agentType: AgentType);
|
||||
set systemPrompt(systemPrompt: string);
|
||||
set userInstruction(userInstruction: string);
|
||||
set toolDefinitions(toolDefinitions: IAIFunctionDefinition[]);
|
||||
set aiFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]);
|
||||
set aiFunctionUsageMode(mode: AIFunctionUsageMode);
|
||||
|
||||
streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown>;
|
||||
formatBinaryFiles(attachments: Attachment[]): string;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { MimeType, toMimeType } from "Enums/MimeType";
|
|||
import { isTextFile } from "Enums/FileType";
|
||||
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class OpenAI extends BaseAIClass {
|
||||
|
||||
|
|
@ -42,13 +43,14 @@ export class OpenAI extends BaseAIClass {
|
|||
|
||||
const tools = [{
|
||||
type: "web_search"
|
||||
}, ...this.mapFunctionDefinitions(this.toolDefinitions)];
|
||||
}, ...this.mapFunctionDefinitions(this.aiFunctionDefinitions)];
|
||||
|
||||
const requestBody = {
|
||||
model: this.model(),
|
||||
instructions: systemPrompt,
|
||||
input: input,
|
||||
tools: tools,
|
||||
tool_choice: this.buildOpenAIToolChoice(),
|
||||
stream: true
|
||||
};
|
||||
|
||||
|
|
@ -350,13 +352,29 @@ export class OpenAI extends BaseAIClass {
|
|||
}]);
|
||||
}
|
||||
|
||||
private buildOpenAIToolChoice(): string {
|
||||
// If no tools defined, fall back to auto
|
||||
if (this.aiFunctionDefinitions.length === 0) {
|
||||
return "auto";
|
||||
}
|
||||
|
||||
switch (this.aiFunctionUsageMode) {
|
||||
case AIFunctionUsageMode.Auto:
|
||||
return "auto";
|
||||
case AIFunctionUsageMode.Enabled:
|
||||
return "required";
|
||||
case AIFunctionUsageMode.Disabled:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
private extractRetryDelay(error: ApiError): number | undefined {
|
||||
if (error.info.type !== ApiErrorType.RATE_LIMIT || !error.info.responseHeaders) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const headers = error.info.responseHeaders;
|
||||
|
||||
|
||||
// 1. Prefer standard Retry-After header (seconds or HTTP-date)
|
||||
const retryAfter = headers.get('retry-after');
|
||||
if (retryAfter) {
|
||||
|
|
@ -365,16 +383,16 @@ export class OpenAI extends BaseAIClass {
|
|||
return Math.max(0, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 2. Fallback to provider-specific headers (e.g., OpenAI)
|
||||
const resetHeader =
|
||||
const resetHeader =
|
||||
headers.get('x-ratelimit-reset-requests') ??
|
||||
headers.get('x-ratelimit-reset-tokens');
|
||||
|
||||
|
||||
if (resetHeader) {
|
||||
return this.parseDurationToSeconds(resetHeader);
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,35 @@ You are a task execution agent. You execute assigned tasks and report outcomes.
|
|||
- Decide that an action is unnecessary
|
||||
- Interpret "if X exists" as permission to skip - always attempt the action
|
||||
- Speculate about future steps or broader plans
|
||||
- **Perform actions beyond what the instruction explicitly states** - even if the context suggests more should be done
|
||||
|
||||
---
|
||||
|
||||
## Scope of Execution
|
||||
|
||||
**CRITICAL: Only perform the SPECIFIC action stated in the instruction. Nothing more.**
|
||||
|
||||
The context is background information to help you understand the task - it is NOT additional instructions. Do not infer additional actions from the context.
|
||||
|
||||
### Instruction vs Context
|
||||
|
||||
| Instruction Says | Context Mentions | Correct Action | WRONG Action |
|
||||
|-----------------|------------------|----------------|--------------|
|
||||
| "Read the template file" | "We're creating an NPC note" | Read the file, report contents | Read AND create the note |
|
||||
| "Search for #project files" | "User wants to summarize them" | Search, report results | Search AND summarize |
|
||||
| "Check if config.json exists" | "We need to update settings" | Check existence, report | Check AND update settings |
|
||||
|
||||
### Why This Matters
|
||||
|
||||
The orchestration agent has broken the work into specific steps for a reason. Each step exists to:
|
||||
- Gather specific information
|
||||
- Perform a specific action
|
||||
- Enable decisions about subsequent steps
|
||||
|
||||
When you perform actions beyond your instruction, you:
|
||||
- Skip the orchestrator's opportunity to make routing decisions
|
||||
- May produce outcomes the user didn't approve
|
||||
- Break the planned workflow's checkpoints
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -148,6 +177,15 @@ The orchestration agent will evaluate your report and make routing decisions. Yo
|
|||
❌ Wrong: "File hasn't changed recently, backup unnecessary"
|
||||
✅ Right: Create the backup, report success
|
||||
|
||||
### Anti-pattern 4: Doing more than instructed
|
||||
|
||||
**Instruction:** "Read the template file at templates/character.md"
|
||||
**Context:** "We are creating a character named 'Bob' using this template"
|
||||
❌ Wrong: Read the template, then create the character file
|
||||
✅ Right: Read the template, report its contents, call CompleteTask
|
||||
|
||||
The context tells you WHY you're reading the template, not that you should also create the character. A later step will handle creation.
|
||||
|
||||
---
|
||||
|
||||
## Signaling Completion
|
||||
|
|
|
|||
|
|
@ -34,35 +34,21 @@
|
|||
autoScroll = true;
|
||||
}
|
||||
|
||||
export function updateChatAreaLayout(behavior: ScrollBehavior | undefined, shouldSettle: boolean = false) {
|
||||
if (layoutUpdateTimeout) {
|
||||
clearTimeout(layoutUpdateTimeout);
|
||||
layoutUpdateTimeout = null;
|
||||
export async function updateChatAreaLayout(behavior: ScrollBehavior | undefined, shouldSettle: boolean = false) {
|
||||
await tick();
|
||||
|
||||
if (!chatAreaPaddingElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const executeLayout = async () => {
|
||||
await tick();
|
||||
|
||||
if (!chatAreaPaddingElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageElements.length <= 0) {
|
||||
chatAreaPaddingElement.style.paddingBottom = "0px";
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
applyLayout(behavior, shouldSettle);
|
||||
});
|
||||
};
|
||||
|
||||
// Instant/settle executes immediately; smooth updates are debounced
|
||||
if (behavior === "instant" || shouldSettle) {
|
||||
executeLayout();
|
||||
} else {
|
||||
layoutUpdateTimeout = setTimeout(executeLayout, 50);
|
||||
if (messageElements.length <= 0) {
|
||||
chatAreaPaddingElement.style.paddingBottom = "0px";
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
applyLayout(behavior, shouldSettle);
|
||||
});
|
||||
}
|
||||
|
||||
function applyLayout(behavior: ScrollBehavior | undefined, shouldSettle: boolean) {
|
||||
|
|
@ -112,7 +98,6 @@
|
|||
let messageElements: { index: number, element: HTMLElement }[] = [];
|
||||
let lastProcessedContent: Map<string, string> = new Map<string, string>();
|
||||
let currentStreamFinalized: boolean = false;
|
||||
let layoutUpdateTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
function getGreetingByTime(): string {
|
||||
const hour = new Date().getHours();
|
||||
|
|
|
|||
5
Enums/AIFunctionUsageMode.ts
Normal file
5
Enums/AIFunctionUsageMode.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export enum AIFunctionUsageMode {
|
||||
Auto = "auto",
|
||||
Enabled = "enabled",
|
||||
Disabled = "disabled"
|
||||
}
|
||||
|
|
@ -14,8 +14,10 @@ import { Services } from "Services/Services";
|
|||
import type { AIFunctionService } from "./AIFunctionService";
|
||||
import type { DebugService } from "Services/DebugService";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class AIController {
|
||||
export abstract class BaseAgent {
|
||||
|
||||
protected ai: IAIClass | undefined;
|
||||
protected readonly aiPrompt: IPrompt;
|
||||
|
|
@ -67,30 +69,22 @@ export class AIController {
|
|||
this.debugService?.log("AgentLoop", `${agentType} agent loop completed`);
|
||||
}
|
||||
|
||||
protected async requestAgentResponse(conversation: Conversation, callbacks: IChatServiceCallbacks): Promise<string> {
|
||||
const response = await this.streamRequestResponse(AgentType.Main, this.ensureCorrectConversationStructure(conversation), callbacks);
|
||||
protected async requestAgentResponse(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks): Promise<string> {
|
||||
return await this.withToolCallingDisabled(async () => {
|
||||
await this.streamRequestResponse(agentType, this.ensureCorrectConversationStructure(conversation), callbacks);
|
||||
|
||||
if (response.functionCall) {
|
||||
conversation.addFunctionResponse(new AIFunctionResponse(
|
||||
response.functionCall.name,
|
||||
{ error: Copy.TextResponseToolDenial },
|
||||
response.functionCall.toolId
|
||||
));
|
||||
return await this.requestAgentResponse(conversation, callbacks);
|
||||
}
|
||||
const lastContent = conversation.contents[conversation.contents.length - 1];
|
||||
const textResponse = lastContent?.content?.trim() ?? "";
|
||||
|
||||
const lastContent = conversation.contents[conversation.contents.length - 1];
|
||||
const textResponse = lastContent?.content?.trim() ?? "";
|
||||
|
||||
if (textResponse === "") {
|
||||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
content: Copy.TextResponseRequired
|
||||
}));
|
||||
return await this.requestAgentResponse(conversation, callbacks);
|
||||
}
|
||||
|
||||
return textResponse;
|
||||
if (textResponse === "") {
|
||||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
content: Copy.TextResponseRequired
|
||||
}));
|
||||
return await this.requestAgentResponse(agentType, conversation, callbacks);
|
||||
}
|
||||
return textResponse;
|
||||
});
|
||||
}
|
||||
|
||||
protected updateThought(functionCall: AIFunctionCall | null, callbacks: IChatServiceCallbacks) {
|
||||
|
|
@ -192,4 +186,18 @@ export class AIController {
|
|||
|
||||
return { functionCall: capturedFunctionCall, shouldContinue: capturedShouldContinue };
|
||||
}
|
||||
|
||||
private async withToolCallingDisabled<T>(callback: () => Promise<T>): Promise<T> {
|
||||
if (!this.ai) { // this shouldn't ever happen
|
||||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
|
||||
const aiFunctionUsageMode = this.ai.aiFunctionUsageMode;
|
||||
this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Disabled;
|
||||
|
||||
const result = await callback();
|
||||
|
||||
this.ai.aiFunctionUsageMode = aiFunctionUsageMode;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ExecutionStep } from "Types/ExecutionStep";
|
||||
import { AIController } from "./AIController";
|
||||
import { BaseAgent } from "./BaseAgent";
|
||||
import { AgentType } from "Enums/AgentType";
|
||||
import { Conversation } from "Conversations/Conversation";
|
||||
import type { IChatServiceCallbacks } from "Services/ChatService";
|
||||
|
|
@ -12,8 +12,9 @@ import { Exception } from "Helpers/Exception";
|
|||
import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
|
||||
import { Copy, replaceCopy } from "Enums/Copy";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class ExecutionAgent extends AIController {
|
||||
export class ExecutionAgent extends BaseAgent {
|
||||
|
||||
private static readonly MAX_AGENT_DEPTH: number = 3;
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ export class ExecutionAgent extends AIController {
|
|||
return { shouldExit: false };
|
||||
}
|
||||
this.debugService?.log("ExecutionAgent", `Task completed (success: ${parseResult.data.success}): ${parseResult.data.description}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
executionResult = parseResult.data;
|
||||
return { shouldExit: true };
|
||||
}
|
||||
|
|
@ -85,9 +87,10 @@ export class ExecutionAgent extends AIController {
|
|||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
this.ai.agentType = AgentType.Execution;
|
||||
this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Enabled;
|
||||
this.ai.systemPrompt = this.aiPrompt.executionInstruction();
|
||||
this.ai.userInstruction = ""; // do not include user instruction for execution agent
|
||||
this.ai.toolDefinitions = AIFunctionDefinitions.executionAgentDefinitions();
|
||||
this.ai.aiFunctionDefinitions = AIFunctionDefinitions.executionAgentDefinitions();
|
||||
}
|
||||
|
||||
protected override setDebugColor(): void {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionD
|
|||
import { Exception } from "Helpers/Exception";
|
||||
import { AIFunction, isAIFunction } from "Enums/AIFunction";
|
||||
import { AgentType } from "Enums/AgentType";
|
||||
import { AIController } from "./AIController";
|
||||
import { BaseAgent } from "./BaseAgent";
|
||||
import { ExecuteWorkflowArgsSchema, type ExecuteWorkflowArgs } from "AIClasses/Schemas/AIFunctionSchemas";
|
||||
import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
|
||||
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
||||
|
|
@ -12,8 +12,9 @@ import { OrchestrationAgent } from "./OrchestrationAgent";
|
|||
import { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class MainAgent extends AIController {
|
||||
export class MainAgent extends BaseAgent {
|
||||
|
||||
public async runMainAgent(conversation: Conversation, allowDestructiveActions: boolean, planningMode: boolean, callbacks: IChatServiceCallbacks) {
|
||||
await this.setAgentPromptAndTools(planningMode, allowDestructiveActions);
|
||||
|
|
@ -88,9 +89,10 @@ export class MainAgent extends AIController {
|
|||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
this.ai.agentType = AgentType.Main;
|
||||
this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Auto;
|
||||
this.ai.systemPrompt = this.aiPrompt.systemInstruction();
|
||||
this.ai.userInstruction = await this.aiPrompt.userInstruction();
|
||||
this.ai.toolDefinitions = AIFunctionDefinitions.agentDefinitions(allowDestructiveActions, planningMode);
|
||||
this.ai.aiFunctionDefinitions = AIFunctionDefinitions.agentDefinitions(allowDestructiveActions, planningMode);
|
||||
}
|
||||
|
||||
protected override setDebugColor(): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CancelPlanArgsSchema, CompletePlanArgsSchema, CompleteStepArgsSchema, ReplanArgsSchema, type ExecuteWorkflowArgs } from "AIClasses/Schemas/AIFunctionSchemas";
|
||||
import { AIController } from "./AIController";
|
||||
import { BaseAgent } from "./BaseAgent";
|
||||
import { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
import type { IChatServiceCallbacks } from "Services/ChatService";
|
||||
|
|
@ -14,8 +14,9 @@ import { AgentType } from "Enums/AgentType";
|
|||
import { AIFunction, isAIFunction } from "Enums/AIFunction";
|
||||
import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class OrchestrationAgent extends AIController {
|
||||
export class OrchestrationAgent extends BaseAgent {
|
||||
|
||||
private static readonly MAX_AGENT_DEPTH: number = 3;
|
||||
|
||||
|
|
@ -121,7 +122,7 @@ export class OrchestrationAgent extends AIController {
|
|||
role: Role.User,
|
||||
content: Copy.RequestPlanSummary
|
||||
}));
|
||||
const orchestrationSummary = await this.requestAgentResponse(planningConversation, callbacks);
|
||||
const orchestrationSummary = await this.requestAgentResponse(AgentType.Orchestration, planningConversation, callbacks);
|
||||
|
||||
return { planExecutionSummary: orchestrationSummary };
|
||||
}
|
||||
|
|
@ -170,6 +171,7 @@ export class OrchestrationAgent extends AIController {
|
|||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `CompleteStep called (confirmed: ${parseResult.data.confirm_completion})`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCallName,
|
||||
{ message: "Step Completed" },
|
||||
|
|
@ -198,6 +200,7 @@ export class OrchestrationAgent extends AIController {
|
|||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `CompletePlan called (confirmed: ${parseResult.data.confirm_completion})`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCallName,
|
||||
{ message: "Plan Completed" },
|
||||
|
|
@ -218,6 +221,7 @@ export class OrchestrationAgent extends AIController {
|
|||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `Replan requested: ${parseResult.data.context}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCallName,
|
||||
{ message: "Replan Requested" },
|
||||
|
|
@ -238,6 +242,7 @@ export class OrchestrationAgent extends AIController {
|
|||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `Plan cancellation requested: ${parseResult.data.context}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCallName,
|
||||
{ message: "Plan Cancelled" },
|
||||
|
|
@ -271,9 +276,10 @@ export class OrchestrationAgent extends AIController {
|
|||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
this.ai.agentType = AgentType.Orchestration;
|
||||
this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Enabled;
|
||||
this.ai.systemPrompt = this.aiPrompt.orchestrationInstruction();
|
||||
this.ai.userInstruction = ""; // do not include user instruction for orchestration agent
|
||||
this.ai.toolDefinitions = AIFunctionDefinitions.orchestrationAgentDefinitions();
|
||||
this.ai.aiFunctionDefinitions = AIFunctionDefinitions.orchestrationAgentDefinitions();
|
||||
}
|
||||
|
||||
protected override setDebugColor(): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Conversation } from "Conversations/Conversation";
|
||||
import { AIController } from "./AIController";
|
||||
import { BaseAgent } from "./BaseAgent";
|
||||
import { AskUserQuestionPlanningArgsSchema, SubmitPlanArgsSchema } from "AIClasses/Schemas/AIFunctionSchemas";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import type { IChatServiceCallbacks } from "Services/ChatService";
|
||||
|
|
@ -12,8 +12,9 @@ import { Copy } from "Enums/Copy";
|
|||
import { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode";
|
||||
|
||||
export class PlanningAgent extends AIController {
|
||||
export class PlanningAgent extends BaseAgent {
|
||||
|
||||
private static readonly MAX_AGENT_DEPTH = 3;
|
||||
|
||||
|
|
@ -110,9 +111,10 @@ export class PlanningAgent extends AIController {
|
|||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
this.ai.agentType = AgentType.Planning;
|
||||
this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Enabled;
|
||||
this.ai.systemPrompt = this.aiPrompt.planningInstruction();
|
||||
this.ai.userInstruction = await this.aiPrompt.userInstruction();
|
||||
this.ai.toolDefinitions = AIFunctionDefinitions.planningAgentDefinitions();
|
||||
this.ai.aiFunctionDefinitions = AIFunctionDefinitions.planningAgentDefinitions();
|
||||
}
|
||||
|
||||
protected override setDebugColor(): void {
|
||||
|
|
|
|||
|
|
@ -85,9 +85,10 @@ export class ChatService {
|
|||
await this.saveConversation(conversation);
|
||||
callbacks.onStreamingUpdate(conversationContent.timestamp.getTime().toString());
|
||||
|
||||
let namingPromise: Promise<void> | undefined;
|
||||
if (firstMessage) {
|
||||
this.onNameChanged?.(conversation.title); // on change for initial conversation name
|
||||
void this.namingService.requestName(conversation, formattedRequest, this.onNameChanged)
|
||||
namingPromise = this.namingService.requestName(conversation, formattedRequest, this.onNameChanged);
|
||||
}
|
||||
|
||||
if (attachments.length > 0) {
|
||||
|
|
@ -107,6 +108,11 @@ export class ChatService {
|
|||
callbacks.onStreamingUpdate(null);
|
||||
|
||||
await this.mainAgent.runMainAgent(conversation, allowDestructiveActions, planningMode, callbacks);
|
||||
|
||||
if (namingPromise) {
|
||||
const timeout = new Promise<void>(resolve => setTimeout(resolve, 5000));
|
||||
await Promise.race([namingPromise, timeout]);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (!AbortService.isAbortError(error)) {
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ export class InputService {
|
|||
public setCursorPosition(element: HTMLElement, position: number, fromEnd: boolean = false) {
|
||||
// Ensure element is focusable
|
||||
if (!element.isContentEditable) {
|
||||
console.warn("Element must be contenteditable");
|
||||
Exception.warn("Element must be contenteditable");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +213,7 @@ export class InputService {
|
|||
|
||||
public insertTextAtCursor(text: string, element?: HTMLElement) {
|
||||
if (element && !element.isContentEditable) {
|
||||
console.warn("Element must be contenteditable");
|
||||
Exception.warn("Element must be contenteditable");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ export class InputService {
|
|||
|
||||
public insertElementAtCursor(node: Node, element?: HTMLElement) {
|
||||
if (element && !element.isContentEditable) {
|
||||
console.warn("Element must be contenteditable");
|
||||
Exception.warn("Element must be contenteditable");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +260,7 @@ export class InputService {
|
|||
|
||||
public deleteTextRange(startPos: number, endPos: number, element: HTMLElement) {
|
||||
if (!element.isContentEditable) {
|
||||
console.warn("Element must be contenteditable");
|
||||
Exception.warn("Element must be contenteditable");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ export class InputService {
|
|||
const endResult = this.findTextNodeAndOffset(element, endPos);
|
||||
|
||||
if (!startResult.node || !endResult.node) {
|
||||
console.warn("Could not find text nodes for range deletion");
|
||||
Exception.warn("Could not find text nodes for range deletion");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { Services } from "./Services";
|
|||
import { Selector } from "Enums/Selector";
|
||||
import type { HTMLService } from "./HTMLService";
|
||||
import type { VaultCacheService } from "./VaultCacheService";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
|
||||
interface IStreamingState {
|
||||
element: HTMLElement;
|
||||
|
|
@ -70,7 +71,7 @@ export class StreamingMarkdownService {
|
|||
const result = this.processor.processSync(preprocessed);
|
||||
return String(result);
|
||||
} catch (error) {
|
||||
console.warn("Markdown processing failed:", error);
|
||||
Exception.warn(`Markdown processing failed:\n${error}`);
|
||||
return this.getFallbackHTML(text);
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +118,7 @@ export class StreamingMarkdownService {
|
|||
this.htmlService.setHTMLContent(state.element, html);
|
||||
state.lastProcessedLength = state.buffer.length;
|
||||
} catch (error) {
|
||||
console.warn("Streaming render failed:", error);
|
||||
Exception.warn(`Streaming render failed:\n${error}`);
|
||||
}
|
||||
|
||||
this.renderTimeouts.delete(messageId);
|
||||
|
|
|
|||
|
|
@ -1157,7 +1157,7 @@ describe('Claude', () => {
|
|||
// Set system prompts before calling streamRequest
|
||||
claude.systemPrompt = 'System instruction';
|
||||
claude.userInstruction = 'User instruction';
|
||||
claude.toolDefinitions = [];
|
||||
claude.aiFunctionDefinitions = [];
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'response', isComplete: true };
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ describe('Gemini', () => {
|
|||
// Set system prompts before calling streamRequest
|
||||
gemini.systemPrompt = 'System instruction';
|
||||
gemini.userInstruction = 'User instruction';
|
||||
gemini.toolDefinitions = [];
|
||||
gemini.aiFunctionDefinitions = [];
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'done', isComplete: true };
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ describe('OpenAI', () => {
|
|||
// Set system prompts before calling streamRequest
|
||||
openai.systemPrompt = 'System instruction';
|
||||
openai.userInstruction = 'User instruction';
|
||||
openai.toolDefinitions = [];
|
||||
openai.aiFunctionDefinitions = [];
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'response', isComplete: true };
|
||||
|
|
|
|||
Loading…
Reference in a new issue