mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add web viewer content retrieval tool for AI agents
Add new get_web_viewer_content tool that allows AI agents to retrieve text content or screenshots from open web views. Refactor AIToolResponse to use AIToolResponsePayload class for structured responses with attachment support. Update tool service to handle binary file attachments consistently across read_vault_files and new web viewer tool.
This commit is contained in:
parent
ae321f382a
commit
3bc2b34aa2
20 changed files with 336 additions and 177 deletions
|
|
@ -51,6 +51,12 @@ export const ListVaultFilesArgsSchema = z.object({
|
|||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const GetWebViewerContentSchema = z.object({
|
||||
url_hint: z.string().optional(),
|
||||
format: z.enum(["text", "screenshot"]),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const ExecuteWorkflowArgsSchema = z.object({
|
||||
goal: z.string(),
|
||||
context: z.string().optional(),
|
||||
|
|
@ -135,8 +141,9 @@ export type WriteVaultFileArgs = z.infer<typeof WriteVaultFileArgsSchema>;
|
|||
export type PatchVaultFileArgs = z.infer<typeof PatchVaultFileArgsSchema>;
|
||||
export type DeleteVaultFilesArgs = z.infer<typeof DeleteVaultFilesArgsSchema>;
|
||||
export type MoveVaultFilesArgs = z.infer<typeof MoveVaultFilesArgsSchema>;
|
||||
export type ListVaultFilesArgs = z.infer<typeof ListVaultFilesArgsSchema>;
|
||||
export type CreateVaultFolder = z.infer<typeof CreateVaultFolderSchema>;
|
||||
export type ListVaultFilesArgs = z.infer<typeof ListVaultFilesArgsSchema>;
|
||||
export type GetWebViewerContent = z.infer<typeof GetWebViewerContentSchema>;
|
||||
export type ExecuteWorkflowArgs = z.infer<typeof ExecuteWorkflowArgsSchema>;
|
||||
export type AskUserQuestionPlanningArgs = z.infer<typeof AskUserQuestionPlanningArgsSchema>;
|
||||
export type AskUserQuestionExecutionArgs = z.infer<typeof AskUserQuestionExecutionArgsSchema>;
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ import { SkipStep } from "./Tools/SkipStep";
|
|||
import { ReadMemories } from "./Tools/ReadMemories";
|
||||
import { UpdateMemories } from "./Tools/UpdateMemories";
|
||||
import { CreateVaultFolder } from "./Tools/CreateVaultFolder";
|
||||
import { GetWebViewerContent } from "./Tools/GetWebViewerContent";
|
||||
|
||||
export abstract class AIToolDefinitions {
|
||||
|
||||
public static isGated: boolean = false;
|
||||
|
||||
// Definitions list provides a list of function definitions that does not include any planning functions (used as reference in planning agent prompt)
|
||||
private static readonly definitionsList = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder];
|
||||
private static readonly definitionsList = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, GetWebViewerContent,
|
||||
WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder];
|
||||
|
||||
public static agentDefinitions(destructive: boolean, planningMode: boolean, memories: boolean, updateMemories: boolean): IAIToolDefinition[] {
|
||||
this.isGated = false;
|
||||
|
|
@ -37,7 +39,8 @@ export abstract class AIToolDefinitions {
|
|||
let actions = [
|
||||
SearchVaultFiles,
|
||||
ReadVaultFiles,
|
||||
ListVaultFiles
|
||||
ListVaultFiles,
|
||||
GetWebViewerContent
|
||||
];
|
||||
|
||||
if (memories) {
|
||||
|
|
@ -66,6 +69,7 @@ export abstract class AIToolDefinitions {
|
|||
SearchVaultFiles,
|
||||
ReadVaultFiles,
|
||||
ListVaultFiles,
|
||||
GetWebViewerContent,
|
||||
CompleteStep,
|
||||
ReviseStep,
|
||||
RevisePlan,
|
||||
|
|
@ -76,7 +80,7 @@ export abstract class AIToolDefinitions {
|
|||
}
|
||||
|
||||
public static planningAgentDefinitions(memories: boolean): IAIToolDefinition[] {
|
||||
let actions = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, AskUserQuestionPlanning, SubmitPlan];
|
||||
let actions = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, GetWebViewerContent, AskUserQuestionPlanning, SubmitPlan];
|
||||
|
||||
if (memories) {
|
||||
actions = actions.concat([ReadMemories]);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
// Platform agnostic class for function responses
|
||||
|
||||
import type { AITool } from "Enums/AITool";
|
||||
import type { AIToolResponsePayload } from "./AIToolResponsePayload";
|
||||
|
||||
// Used by AI providers to format function execution results for API calls
|
||||
export class AIToolResponse {
|
||||
public readonly name: AITool;
|
||||
public readonly response: object;
|
||||
public readonly payload: AIToolResponsePayload;
|
||||
public readonly toolId?: string;
|
||||
|
||||
public static readonly UserRejectionMessage: string = `The user has explicitly rejected this change.
|
||||
They may have changed their mind about the requested change.
|
||||
|
||||
**CRITICAL:** Immediately stop all further actions and consult with the user`;
|
||||
|
||||
|
||||
public static readonly UserSuggestionMessage: string = `**USER MODIFICATION REQUEST:**
|
||||
|
||||
The user has reviewed your proposed action and provided a modification or alternative direction.
|
||||
|
|
@ -29,9 +30,9 @@ export class AIToolResponse {
|
|||
|
||||
**User's Suggestion:**`;
|
||||
|
||||
constructor(name: AITool, response: object, toolId?: string) {
|
||||
constructor(name: AITool, payload: AIToolResponsePayload, toolId?: string) {
|
||||
this.name = name;
|
||||
this.response = response;
|
||||
this.payload = payload;
|
||||
this.toolId = toolId;
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ export class AIToolResponse {
|
|||
id: this.toolId,
|
||||
functionResponse: {
|
||||
name: this.name,
|
||||
response: { result: this.response }
|
||||
response: { result: this.payload.response }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
11
AIClasses/ToolDefinitions/AIToolResponsePayload.ts
Normal file
11
AIClasses/ToolDefinitions/AIToolResponsePayload.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { Attachment } from "Conversations/Attachment";
|
||||
|
||||
export class AIToolResponsePayload {
|
||||
public readonly response: object;
|
||||
public readonly attachments: Attachment[];
|
||||
|
||||
constructor(response: object, attachments: Attachment[] = []) {
|
||||
this.response = response;
|
||||
this.attachments = attachments;
|
||||
}
|
||||
}
|
||||
31
AIClasses/ToolDefinitions/Tools/GetWebViewerContent.ts
Normal file
31
AIClasses/ToolDefinitions/Tools/GetWebViewerContent.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { AITool } from "Enums/AITool";
|
||||
import type { IAIToolDefinition } from "../IAIToolDefinition";
|
||||
|
||||
export const GetWebViewerContent: IAIToolDefinition = {
|
||||
name: AITool.GetWebViewerContent,
|
||||
description: `Retrieves the text content from a currently open Web Viewer tab in Obsidian.
|
||||
|
||||
Call this function:
|
||||
- When the user asks you to read, summarise, or act on the contents of an open web page
|
||||
- When you need to extract information from a URL that is already open in the Web Viewer
|
||||
- When the user refers to "this page", "the current tab", or similar`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url_hint: {
|
||||
type: "string",
|
||||
description: "Optional. A partial URL or domain to identify which Web Viewer tab to read from, if multiple tabs are open. If omitted, the active or most recently opened Web Viewer tab will be used. Example: 'github.com'"
|
||||
},
|
||||
format: {
|
||||
type: "string",
|
||||
enum: ["text", "screenshot"],
|
||||
description: "The format in which to return the page content. Use 'text' for plain readable content (equivalent to document.body.innerText), or 'screenshot' for an image of the webpage. Prefer 'text' unless you need to inspect the page visually."
|
||||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
description: "A short message to be displayed to the user explaining what you're reading and why. Example: 'Reading the open page to summarise its key points'"
|
||||
}
|
||||
},
|
||||
required: ["format", "user_message"]
|
||||
}
|
||||
}
|
||||
|
|
@ -478,6 +478,10 @@ After multi-step execution:
|
|||
- Problem-solving and explanations across any domain
|
||||
- Programming, writing, and creative tasks with vault context
|
||||
|
||||
**Web Viewer**
|
||||
- When the user asks about a web page or its contents, immediately attempt to retrieve the open web view — do not ask for a URL first
|
||||
- Retrieving web view content is always safe: if no page is open, the tool will tell you — there is no risk in trying
|
||||
|
||||
**Interactive Capabilities**
|
||||
- Asking clarifying questions during planning to shape the approach
|
||||
- Consulting the user during execution when decisions require their input
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import { StringTools } from "Helpers/StringTools";
|
||||
import { ConversationContent } from "./ConversationContent";
|
||||
import { Attachment } from "./Attachment";
|
||||
import type { AIToolResponse } from "AIClasses/ToolDefinitions/AIToolResponse";
|
||||
import { Role } from "Enums/Role";
|
||||
import { AITool } from "Enums/AITool";
|
||||
import { isTextFile, toFileType } from "Enums/FileType";
|
||||
import { FileTypeToMimeType } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { isDocumentMimeType, MimeType } from "Enums/MimeType";
|
||||
|
||||
export class Conversation {
|
||||
|
||||
|
|
@ -28,95 +23,17 @@ export class Conversation {
|
|||
}
|
||||
|
||||
public addFunctionResponse(functionResponse: AIToolResponse): void {
|
||||
if (functionResponse.name !== AITool.ReadVaultFiles) {
|
||||
const functionResponseString = functionResponse.toConversationString();
|
||||
this.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
functionResponse: functionResponseString,
|
||||
shouldDisplayContent: false,
|
||||
toolId: functionResponse.toolId
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const responseData = functionResponse.response as
|
||||
{results?: Array<{type?: string, path: string, contents?: string, error?: string}>};
|
||||
const results = responseData.results;
|
||||
|
||||
if (!results) {
|
||||
// Handle case where results array is missing (general error response)
|
||||
const functionResponseString = functionResponse.toConversationString();
|
||||
this.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
functionResponse: functionResponseString,
|
||||
shouldDisplayContent: false,
|
||||
toolId: functionResponse.toolId
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate error results from successful file reads
|
||||
const errorResults = results.filter(result => result.error);
|
||||
const successResults = results.filter(result => !result.error && result.type && result.contents !== undefined);
|
||||
|
||||
const textResults = successResults.filter(result => isTextFile(result.type!));
|
||||
const binaryResults = successResults.filter(result => !isTextFile(result.type!));
|
||||
|
||||
// 1. Function response with text files and/or errors
|
||||
let functionResponseData;
|
||||
if (textResults.length > 0 || errorResults.length > 0) {
|
||||
// Include text file contents and any errors in function response
|
||||
const responseResults = [...textResults, ...errorResults];
|
||||
functionResponseData = {
|
||||
id: functionResponse.toolId,
|
||||
functionResponse: {
|
||||
name: functionResponse.name,
|
||||
response: {
|
||||
results: responseResults,
|
||||
...(binaryResults.length > 0 && {message: "The contents of the files are included below."})
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// No text files or errors - just binary files
|
||||
functionResponseData = {
|
||||
id: functionResponse.toolId,
|
||||
functionResponse: {
|
||||
name: functionResponse.name,
|
||||
response: {
|
||||
message: "Files retrieved successfully. The contents of the files are included below.",
|
||||
count: binaryResults.length
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
functionResponse: JSON.stringify(functionResponseData),
|
||||
functionResponse: functionResponse.toConversationString(),
|
||||
shouldDisplayContent: false,
|
||||
toolId: functionResponse.toolId
|
||||
}));
|
||||
|
||||
// 2. If there are binary files, create Attachments and add to conversation
|
||||
if (binaryResults.length > 0) {
|
||||
const attachments = binaryResults
|
||||
.filter(file => file.type && file.contents !== undefined)
|
||||
.map(file => {
|
||||
const fileName = file.path.split('/').pop() || file.path;
|
||||
let mimeType = FileTypeToMimeType[toFileType(file.type as string)];
|
||||
|
||||
if (isDocumentMimeType(mimeType)) {
|
||||
mimeType = MimeType.TEXT_PLAIN;
|
||||
return new Attachment(fileName, mimeType, StringTools.toBase64(file.contents as string));
|
||||
}
|
||||
|
||||
return new Attachment(fileName, mimeType, file.contents as string);
|
||||
});
|
||||
|
||||
if (functionResponse.payload.attachments.length > 0) {
|
||||
this.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
attachments: attachments,
|
||||
attachments: functionResponse.payload.attachments,
|
||||
shouldDisplayContent: false
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export enum AITool {
|
|||
ListVaultFiles = "list_vault_files",
|
||||
ReadMemories = "read_memories",
|
||||
UpdateMemories = "update_memories",
|
||||
GetWebViewerContent = "get_web_viewer_content",
|
||||
|
||||
// used by gemini and mistral
|
||||
RequestWebSearch = "request_web_search",
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ The following context explains why you are doing the task. It is NOT an instruct
|
|||
MemoriesUpdatedSuccess = "Memories have been updated successfully.",
|
||||
MemoriesDisabledError = "Illegal tool call, memories have been disabled by the user.",
|
||||
MemoriesUpdatingDisabledError = "Illegal tool call, updating memories has been disabled by the user.",
|
||||
WebViewerNoMatchingUrl = "No open web view was found matching the URL '{urlHint}'. Ensure the correct page is open in the web viewer.",
|
||||
WebViewerNoOpenView = "No open web view was found. Ask the user to open a page in the web viewer and try again.",
|
||||
PlanSubmissionRequired = "Error: Attempted to exit planning but plan has not yet been submitted!",
|
||||
MaxExecutionDepthReached = "Exceeded maximum plan execution attempts - consult with the user on how to continue.",
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,18 @@ import type { ISearchMatch } from "../../Types/SearchTypes";
|
|||
import { AbortService } from "../AbortService";
|
||||
import { normalizePath, TAbstractFile, TFile } from "obsidian";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import { Copy, replaceCopy } from "Enums/Copy";
|
||||
import { pathExtname } from "Helpers/Helpers";
|
||||
import type { MemoriesService } from "Services/MemoriesService";
|
||||
import {
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import type { WebViewerService } from "Services/WebViewerService";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
import { Attachment } from "Conversations/Attachment";
|
||||
import { isDocumentMimeType, MimeType } from "Enums/MimeType";
|
||||
import { isTextFile, toFileType } from "Enums/FileType";
|
||||
import { FileTypeToMimeType } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { StringTools } from "Helpers/StringTools";
|
||||
import {
|
||||
SearchVaultFilesArgsSchema,
|
||||
ReadVaultFilesArgsSchema,
|
||||
WriteVaultFileArgsSchema,
|
||||
|
|
@ -21,15 +29,16 @@ import {
|
|||
PatchVaultFileArgsSchema,
|
||||
ReadMemoriesArgsSchema,
|
||||
UpdateMemoriesArgsSchema,
|
||||
CreateVaultFolderSchema
|
||||
CreateVaultFolderSchema,
|
||||
GetWebViewerContentSchema
|
||||
} from "AIClasses/Schemas/AIToolSchemas";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
|
||||
export class AIToolService {
|
||||
|
||||
private readonly fileSystemService: FileSystemService;
|
||||
private readonly memoriesService: MemoriesService;
|
||||
private readonly settingsService: SettingsService;
|
||||
private readonly webViewerService: WebViewerService;
|
||||
private readonly abortService: AbortService;
|
||||
|
||||
private lastToolReadMemories: boolean = false;
|
||||
|
|
@ -38,6 +47,7 @@ export class AIToolService {
|
|||
this.fileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
||||
this.memoriesService = Resolve<MemoriesService>(Services.MemoriesService);
|
||||
this.settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
this.webViewerService = Resolve<WebViewerService>(Services.WebViewerService);
|
||||
this.abortService = Resolve<AbortService>(Services.AbortService);
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +64,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.SearchVaultFiles}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.SearchVaultFiles}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -66,7 +76,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.ReadVaultFiles}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.ReadVaultFiles}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -78,7 +88,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.WriteVaultFile}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.WriteVaultFile}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -90,7 +100,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.PatchVaultFile}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.PatchVaultFile}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -102,7 +112,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.DeleteVaultFiles}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.DeleteVaultFiles}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -114,7 +124,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.MoveVaultFiles}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.MoveVaultFiles}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -126,7 +136,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.CreateVaultFolder}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.CreateVaultFolder}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -138,19 +148,31 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.ListVaultFiles}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.ListVaultFiles}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(toolCall.name, await this.listVaultFiles(parseResult.data.path, parseResult.data.recursive), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.GetWebViewerContent: {
|
||||
const parseResult = GetWebViewerContentSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.GetWebViewerContent}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(toolCall.name, await this.getWebViewerContent(parseResult.data.format, parseResult.data.url_hint), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.ReadMemories: {
|
||||
const parseResult = ReadMemoriesArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.ReadMemories}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.ReadMemories}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -162,7 +184,7 @@ export class AIToolService {
|
|||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.UpdateMemories}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.UpdateMemories}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -171,7 +193,7 @@ export class AIToolService {
|
|||
|
||||
// This is only used by gemini
|
||||
case AITool.RequestWebSearch:
|
||||
return new AIToolResponse(toolCall.name, {}, toolCall.toolId)
|
||||
return new AIToolResponse(toolCall.name, new AIToolResponsePayload({}), toolCall.toolId)
|
||||
|
||||
// multi-agent functions are handled elsewhere - this shouldn't really ever get hit
|
||||
case AITool.ExecuteWorkflow:
|
||||
|
|
@ -189,7 +211,7 @@ export class AIToolService {
|
|||
Exception.log(`Multi-agent function ${toolCall.name} should not be handled by AIToolService`);
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Failed to execute ${toolCall.name}.` },
|
||||
new AIToolResponsePayload({ error: `Failed to execute ${toolCall.name}.` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -201,7 +223,7 @@ export class AIToolService {
|
|||
Exception.log(error);
|
||||
return new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: error },
|
||||
new AIToolResponsePayload({ error: error }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -209,7 +231,7 @@ export class AIToolService {
|
|||
});
|
||||
}
|
||||
|
||||
private async searchVaultFiles(searchTerms: string[]): Promise<object> {
|
||||
private async searchVaultFiles(searchTerms: string[]): Promise<AIToolResponsePayload> {
|
||||
const results: { searchTerm: string, results: object[] }[] = [];
|
||||
|
||||
for (const searchTerm of searchTerms) {
|
||||
|
|
@ -227,10 +249,10 @@ export class AIToolService {
|
|||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
return new AIToolResponsePayload(results);
|
||||
}
|
||||
|
||||
private async readVaultFiles(filePaths: string[]): Promise<object> {
|
||||
private async readVaultFiles(filePaths: string[]): Promise<AIToolResponsePayload> {
|
||||
const results = await Promise.all(
|
||||
filePaths.map(async (filePath) => {
|
||||
const result = await this.fileSystemService.readFile(filePath);
|
||||
|
|
@ -244,28 +266,56 @@ export class AIToolService {
|
|||
};
|
||||
})
|
||||
);
|
||||
return { results };
|
||||
|
||||
const errorResults = results.filter(result => result.error);
|
||||
const successResults = results.filter(result => !result.error && result.type && result.contents !== undefined) as
|
||||
Array<{ type: string; path: string; contents: string }>;
|
||||
|
||||
const textResults = successResults.filter(result => isTextFile(result.type));
|
||||
const binaryResults = successResults.filter(result => !isTextFile(result.type));
|
||||
|
||||
const attachments = binaryResults.map(file => {
|
||||
const fileName = file.path.split('/').pop() || file.path;
|
||||
let mimeType = FileTypeToMimeType[toFileType(file.type)];
|
||||
if (isDocumentMimeType(mimeType)) {
|
||||
mimeType = MimeType.TEXT_PLAIN;
|
||||
return new Attachment(fileName, mimeType, StringTools.toBase64(file.contents));
|
||||
}
|
||||
return new Attachment(fileName, mimeType, file.contents);
|
||||
});
|
||||
|
||||
const response = textResults.length > 0 || errorResults.length > 0
|
||||
? {
|
||||
results: [...textResults, ...errorResults],
|
||||
...(binaryResults.length > 0 && { message: "The contents of the files are included below." })
|
||||
}
|
||||
: {
|
||||
message: "Files retrieved successfully. The contents of the files are included below.",
|
||||
count: binaryResults.length
|
||||
};
|
||||
|
||||
return new AIToolResponsePayload(response, attachments);
|
||||
}
|
||||
|
||||
private async writeVaultFile(filePath: string, content: string): Promise<object> {
|
||||
private async writeVaultFile(filePath: string, content: string): Promise<AIToolResponsePayload> {
|
||||
const result = await this.fileSystemService.writeFile(normalizePath(filePath), content);
|
||||
if (result instanceof Error) {
|
||||
return { success: false, error: result.message };
|
||||
return new AIToolResponsePayload({ success: false, error: result.message });
|
||||
}
|
||||
return { success: true };
|
||||
return new AIToolResponsePayload({ success: true });
|
||||
}
|
||||
|
||||
private async patchVaultFile(filePath: string, oldContent: string[], newContent: string[]): Promise<object> {
|
||||
private async patchVaultFile(filePath: string, oldContent: string[], newContent: string[]): Promise<AIToolResponsePayload> {
|
||||
const result = await this.fileSystemService.patchFile(normalizePath(filePath), oldContent, newContent);
|
||||
if (result instanceof Error) {
|
||||
return { success: false, error: result.message };
|
||||
return new AIToolResponsePayload({ success: false, error: result.message });
|
||||
}
|
||||
return { success: true };
|
||||
return new AIToolResponsePayload({ success: true });
|
||||
}
|
||||
|
||||
private async deleteVaultFiles(filePaths: string[], confirmation: boolean): Promise<object> {
|
||||
private async deleteVaultFiles(filePaths: string[], confirmation: boolean): Promise<AIToolResponsePayload> {
|
||||
if (!confirmation) {
|
||||
return { error: "Confirmation was false, no action taken" };
|
||||
return new AIToolResponsePayload({ error: "Confirmation was false, no action taken" });
|
||||
}
|
||||
|
||||
const results = await Promise.all(filePaths.map(async filePath => {
|
||||
|
|
@ -276,12 +326,12 @@ export class AIToolService {
|
|||
return { path: filePath, success: true };
|
||||
}));
|
||||
|
||||
return { results };
|
||||
return new AIToolResponsePayload({ results });
|
||||
}
|
||||
|
||||
private async moveVaultFiles(sourcePaths: string[], destinationPaths: string[]): Promise<object> {
|
||||
private async moveVaultFiles(sourcePaths: string[], destinationPaths: string[]): Promise<AIToolResponsePayload> {
|
||||
if (sourcePaths.length !== destinationPaths.length) {
|
||||
return { error: "Source paths array length does not equal destination paths array length" };
|
||||
return new AIToolResponsePayload({ error: "Source paths array length does not equal destination paths array length" });
|
||||
}
|
||||
|
||||
const results = await Promise.all(sourcePaths.map(async (sourcePath, index) => {
|
||||
|
|
@ -293,45 +343,60 @@ export class AIToolService {
|
|||
return { path: destinationPath, success: true };
|
||||
}));
|
||||
|
||||
return { results };
|
||||
return new AIToolResponsePayload({ results });
|
||||
}
|
||||
|
||||
private async createVaultFolder(path: string): Promise<object> {
|
||||
private async createVaultFolder(path: string): Promise<AIToolResponsePayload> {
|
||||
const result = await this.fileSystemService.createFolder(path);
|
||||
if (result instanceof Error) {
|
||||
return { path: path, success: false, error: result.message };
|
||||
return new AIToolResponsePayload({ path: path, success: false, error: result.message });
|
||||
}
|
||||
return { path: path, success: true };
|
||||
return new AIToolResponsePayload({ path: path, success: true });
|
||||
}
|
||||
|
||||
private async listVaultFiles(path: string, recursive: boolean): Promise<object> {
|
||||
private async listVaultFiles(path: string, recursive: boolean): Promise<AIToolResponsePayload> {
|
||||
const files: TAbstractFile[] = await this.fileSystemService.listDirectoryContents(path, recursive);
|
||||
return files.map(file => ({
|
||||
return new AIToolResponsePayload(files.map(file => ({
|
||||
type: file instanceof TFile ? "file" : "directory",
|
||||
path: file.path
|
||||
}));
|
||||
})));
|
||||
}
|
||||
|
||||
private async readMemories(): Promise<object> {
|
||||
private async getWebViewerContent(format: "text" | "screenshot", urlHint?: string): Promise<AIToolResponsePayload> {
|
||||
const result = format === "text"
|
||||
? await this.webViewerService.getWebViewContent(urlHint)
|
||||
: await this.webViewerService.takeScreenshot(urlHint, true);
|
||||
|
||||
if (urlHint && !result) {
|
||||
return new AIToolResponsePayload({ error: replaceCopy(Copy.WebViewerNoMatchingUrl, [urlHint]) });
|
||||
}
|
||||
if (!result) {
|
||||
return new AIToolResponsePayload({ error: Copy.WebViewerNoOpenView });
|
||||
}
|
||||
|
||||
return format === "text"
|
||||
? new AIToolResponsePayload({ content: result })
|
||||
: new AIToolResponsePayload({ success: true }, [new Attachment("screenshot.png", MimeType.IMAGE_PNG, result)]);
|
||||
}
|
||||
|
||||
private async readMemories(): Promise<AIToolResponsePayload> {
|
||||
if (!this.settingsService.settings.enableMemories) {
|
||||
return { error: Copy.MemoriesDisabledError }
|
||||
return new AIToolResponsePayload({ error: Copy.MemoriesDisabledError });
|
||||
}
|
||||
this.lastToolReadMemories = true;
|
||||
return { memories: await this.memoriesService.readMemories() };
|
||||
return new AIToolResponsePayload({ memories: await this.memoriesService.readMemories() });
|
||||
}
|
||||
|
||||
private async updateMemories(content: string): Promise<object> {
|
||||
private async updateMemories(content: string): Promise<AIToolResponsePayload> {
|
||||
if (!this.settingsService.settings.allowUpdatingMemories) {
|
||||
return { error: Copy.MemoriesUpdatingDisabledError };
|
||||
return new AIToolResponsePayload({ error: Copy.MemoriesUpdatingDisabledError });
|
||||
}
|
||||
|
||||
if (!this.lastToolReadMemories) {
|
||||
return {
|
||||
error: Copy.UpdateMemoriesWithoutReadError
|
||||
};
|
||||
return new AIToolResponsePayload({ error: Copy.UpdateMemoriesWithoutReadError });
|
||||
}
|
||||
this.lastToolReadMemories = false;
|
||||
|
||||
return { result: await this.memoriesService.updateMemories(content) };
|
||||
return new AIToolResponsePayload({ result: await this.memoriesService.updateMemories(content) });
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { AIToolDefinitions } from "AIClasses/ToolDefinitions/AIToolDefinitions";
|
|||
import { Copy, replaceCopy } from "Enums/Copy";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
|
||||
export class ExecutionAgent extends BaseAgent {
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ export class ExecutionAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
this.conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CompleteTask}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.CompleteTask}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
|
|
@ -61,7 +62,7 @@ export class ExecutionAgent extends BaseAgent {
|
|||
this.debugService?.log("ExecutionAgent", `Task completed (success: ${parseResult.data.success}): ${parseResult.data.description}`);
|
||||
this.conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ result: parseResult.data.success ? "Task completed successfully." : "Task failed, attempting recovery..." },
|
||||
new AIToolResponsePayload({ result: parseResult.data.success ? "Task completed successfully." : "Task failed, attempting recovery..." }),
|
||||
toolCall.toolId
|
||||
));
|
||||
this.updateThought(toolCall, callbacks);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { ConversationContent } from "Conversations/ConversationContent";
|
|||
import { Role } from "Enums/Role";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
|
||||
export class MainAgent extends BaseAgent {
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ export class MainAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.ExecuteWorkflow}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.ExecuteWorkflow}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { AITool, isAITool } from "Enums/AITool";
|
|||
import { AIToolResponse } from "AIClasses/ToolDefinitions/AIToolResponse";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
|
||||
export class OrchestrationAgent extends BaseAgent {
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
|
||||
private orchestrationDepth: number = 0;
|
||||
|
||||
public async runPlannedWorkflow(planRequest: ExecuteWorkflowArgs, callbacks: IChatServiceCallbacks): Promise<object> {
|
||||
public async runPlannedWorkflow(planRequest: ExecuteWorkflowArgs, callbacks: IChatServiceCallbacks): Promise<AIToolResponsePayload> {
|
||||
this.debugService?.log("OrchestrationAgent", `Starting planned workflow: ${planRequest.goal}`);
|
||||
const planningConversation: Conversation = new Conversation();
|
||||
planningConversation.contents.push(new ConversationContent({
|
||||
|
|
@ -41,7 +42,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
|
||||
if (!executionPlan) {
|
||||
this.debugService?.log("OrchestrationAgent", "Planning failed - no execution plan generated");
|
||||
return { message: Copy.PlanningFailedNoSteps };
|
||||
return new AIToolResponsePayload({ message: Copy.PlanningFailedNoSteps });
|
||||
}
|
||||
this.debugService?.log("OrchestrationAgent", `Execution plan received with ${executionPlan.executionSteps.length} steps`);
|
||||
callbacks.onPlanUpdate(executionPlan);
|
||||
|
|
@ -61,7 +62,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
|
||||
if (!executionResult) {
|
||||
this.debugService?.log("OrchestrationAgent", `Step ${stepIndex + 1} failed to execute - workflow aborted`);
|
||||
return { message: replaceCopy(Copy.WorkflowFailedAtStep, [step.description]) };
|
||||
return new AIToolResponsePayload({ message: replaceCopy(Copy.WorkflowFailedAtStep, [step.description]) });
|
||||
}
|
||||
|
||||
if (executionResult.success) {
|
||||
|
|
@ -127,7 +128,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
|
||||
if (orchestrationResult.abort) {
|
||||
this.debugService?.log("OrchestrationAgent", `Orchestration decision: ABORT — ${orchestrationResult.abortContext}`);
|
||||
return { message: replaceCopy(Copy.WorkflowAborted, [orchestrationResult.abortContext]) };
|
||||
return new AIToolResponsePayload({ message: replaceCopy(Copy.WorkflowAborted, [orchestrationResult.abortContext]) });
|
||||
}
|
||||
|
||||
if (orchestrationResult.complete) {
|
||||
|
|
@ -144,7 +145,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
}));
|
||||
const orchestrationSummary = await this.requestAgentResponse(AgentType.Orchestration, planningConversation, callbacks);
|
||||
|
||||
return { planExecutionSummary: orchestrationSummary };
|
||||
return new AIToolResponsePayload({ planExecutionSummary: orchestrationSummary });
|
||||
}
|
||||
|
||||
private async runOrchestrationAgentLoop(planningConversation: Conversation, callbacks: IChatServiceCallbacks): Promise<OrchestrationResult> {
|
||||
|
|
@ -166,7 +167,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
this.debugService?.log("Orchestration", `Invalid tool call denied: ${toolCallName}`);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: Copy.OrchestrationToolDenial },
|
||||
new AIToolResponsePayload({ message: Copy.OrchestrationToolDenial }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -188,7 +189,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CompleteStep}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.CompleteStep}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -196,7 +197,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.data.confirm_completion) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: "Confirmation was false, no action taken" },
|
||||
new AIToolResponsePayload({ error: "Confirmation was false, no action taken" }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -205,7 +206,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: "Step completed" },
|
||||
new AIToolResponsePayload({ message: "Step completed" }),
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({ continue: true, continueContext: parseResult.data.context_for_next_step });
|
||||
|
|
@ -217,7 +218,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.ReviseStep}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.ReviseStep}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -226,7 +227,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: "Step revision accepted — retrying" },
|
||||
new AIToolResponsePayload({ message: "Step revision accepted — retrying" }),
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({
|
||||
|
|
@ -243,7 +244,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.RevisePlan}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.RevisePlan}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -252,7 +253,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: `Plan revised with ${parseResult.data.steps.length} remaining step(s)` },
|
||||
new AIToolResponsePayload({ message: `Plan revised with ${parseResult.data.steps.length} remaining step(s)` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({
|
||||
|
|
@ -267,7 +268,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.SkipStep}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.SkipStep}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -276,7 +277,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: "Step skipped" },
|
||||
new AIToolResponsePayload({ message: "Step skipped" }),
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({
|
||||
|
|
@ -291,7 +292,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CompletePlan}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.CompletePlan}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -299,7 +300,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.data.confirm_completion) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: "Confirmation was false, no action taken" },
|
||||
new AIToolResponsePayload({ error: "Confirmation was false, no action taken" }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -308,7 +309,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: "Plan completed" },
|
||||
new AIToolResponsePayload({ message: "Plan completed" }),
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({ complete: true });
|
||||
|
|
@ -320,7 +321,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CancelPlan}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.CancelPlan}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
|
|
@ -329,7 +330,7 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: "Plan cancelled" },
|
||||
new AIToolResponsePayload({ message: "Plan cancelled" }),
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({ abort: true, abortContext: parseResult.data.context });
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { ConversationContent } from "Conversations/ConversationContent";
|
|||
import { Role } from "Enums/Role";
|
||||
import { DebugColor } from "Enums/DebugColor";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
|
||||
export class PlanningAgent extends BaseAgent {
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ export class PlanningAgent extends BaseAgent {
|
|||
this.debugService?.log("PlanningAgent", `Invalid tool call denied: ${toolCallName}`);
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: Copy.PlanningToolDenial },
|
||||
new AIToolResponsePayload({ message: Copy.PlanningToolDenial }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
|
|
@ -50,7 +51,7 @@ export class PlanningAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.AskUserQuestionPlanning}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.AskUserQuestionPlanning}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
|
|
@ -61,7 +62,7 @@ export class PlanningAgent extends BaseAgent {
|
|||
this.debugService?.log("PlanningAgent", "User answer received");
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ answer: answer },
|
||||
new AIToolResponsePayload({ answer: answer }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
|
|
@ -72,7 +73,7 @@ export class PlanningAgent extends BaseAgent {
|
|||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.SubmitPlan}: ${parseResult.error.message}` },
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.SubmitPlan}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
|
|
@ -81,7 +82,7 @@ export class PlanningAgent extends BaseAgent {
|
|||
capturedPlan = new ExecutionPlan(parseResult.data);
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
toolCallName,
|
||||
{ message: Copy.PlanReceived },
|
||||
new AIToolResponsePayload({ message: Copy.PlanReceived }),
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: true };
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { UserInputService } from "./UserInputService";
|
|||
import { VaultCacheService } from "./VaultCacheService";
|
||||
import { VaultService } from "./VaultService";
|
||||
import { WorkSpaceService } from "./WorkSpaceService";
|
||||
import { WebViewerService } from "./WebViewerService";
|
||||
|
||||
// Stores
|
||||
import { ExecutionPlanStore } from "Stores/ExecutionPlanStore";
|
||||
|
|
@ -81,7 +82,9 @@ export function RegisterDependencies() {
|
|||
RegisterSingleton<MemoriesService>(Services.MemoriesService, new MemoriesService());
|
||||
RegisterSingleton<ConversationFileSystemService>(Services.ConversationFileSystemService, new ConversationFileSystemService());
|
||||
RegisterSingleton<ConversationNamingService>(Services.ConversationNamingService, new ConversationNamingService());
|
||||
|
||||
|
||||
RegisterTransient<WebViewerService>(Services.WebViewerService, () => new WebViewerService());
|
||||
|
||||
RegisterSingleton<IPrompt>(Services.IPrompt, new AIPrompt());
|
||||
RegisterSingleton<AIToolService>(Services.AIToolService, new AIToolService());
|
||||
RegisterSingleton<MainAgent>(Services.MainAgent, new MainAgent());
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export class Services {
|
|||
static ChatService = Symbol("ChatService");
|
||||
static SanitiserService = Symbol("SanitiserService");
|
||||
static InputService = Symbol("InputService");
|
||||
static WebViewerService = Symbol("WebViewerService");
|
||||
static DiffService = Symbol("DiffService");
|
||||
static MemoriesService = Symbol("MemoriesService");
|
||||
static DebugService = Symbol("DebugService");
|
||||
|
|
|
|||
86
Services/WebViewerService.ts
Normal file
86
Services/WebViewerService.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import type VaultkeeperAIPlugin from "main";
|
||||
import { Resolve } from "./DependencyService";
|
||||
import { Services } from "./Services";
|
||||
import type { WebviewElement } from "Types/WebviewElement";
|
||||
|
||||
export class WebViewerService {
|
||||
|
||||
private readonly plugin: VaultkeeperAIPlugin;
|
||||
|
||||
public constructor() {
|
||||
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
}
|
||||
|
||||
public async getWebViewContent(urlHint?: string): Promise<string | null> {
|
||||
const webviewElement = this.getWebviewElement(urlHint);
|
||||
|
||||
if (!webviewElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await this.waitForLoad(webviewElement)) {
|
||||
const pageContent = await webviewElement.executeJavaScript(
|
||||
'document.body.innerText'
|
||||
);
|
||||
return pageContent as string ?? "Failed to retrieve page content";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public async takeScreenshot(urlHint?: string, stripDataUrl: boolean = false): Promise<string | null> {
|
||||
const webviewElement = this.getWebviewElement(urlHint);
|
||||
|
||||
if (!webviewElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await this.waitForLoad(webviewElement)) {
|
||||
const screenshot = await webviewElement.capturePage();
|
||||
const dataUrl = screenshot.toDataURL();
|
||||
return stripDataUrl ? dataUrl.split(',')[1] : dataUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getWebviewElement(urlHint?: string): WebviewElement | null {
|
||||
const leaves = this.plugin.app.workspace.getLeavesOfType('webviewer');
|
||||
if (leaves.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let webviewElement: WebviewElement | null = null;
|
||||
if (urlHint) {
|
||||
for (const leaf of leaves) {
|
||||
const element = leaf.view.containerEl.querySelector('webview');
|
||||
if (this.isWebviewElement(element) && element.getURL().contains(urlHint)) {
|
||||
webviewElement = element;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const leaf = leaves[0];
|
||||
const element = leaf.view.containerEl.querySelector('webview');
|
||||
if (this.isWebviewElement(element)) {
|
||||
webviewElement = element;
|
||||
}
|
||||
}
|
||||
return webviewElement;
|
||||
}
|
||||
|
||||
private async waitForLoad(webviewElement: WebviewElement, timeoutMs = 10000): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
while (webviewElement.isLoading()) {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
return false;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private isWebviewElement(element: Element | null): element is WebviewElement {
|
||||
return element !== null
|
||||
&& typeof (element as WebviewElement).isLoading === 'function'
|
||||
&& typeof (element as WebviewElement).executeJavaScript === 'function';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -57,14 +57,18 @@
|
|||
resize: none;
|
||||
}
|
||||
|
||||
.setting-item-memories-disabled .checkbox-container {
|
||||
background-color: var(--interactive-accent-disabled);
|
||||
.setting-item-memories-disabled-accent .checkbox-container {
|
||||
background-color: var(--interactive-accent-disabled-accent);
|
||||
}
|
||||
|
||||
.setting-item-memories-disabled .setting-item-info {
|
||||
.setting-item-memories-disabled-accent .setting-item-info {
|
||||
font-weight: var(--font-light);
|
||||
}
|
||||
|
||||
.setting-item-memories-disabled .checkbox-container {
|
||||
background-color: var(--interactive-accent-disabled);
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* CSS Variables for Common Components */
|
||||
/* ============================== */
|
||||
|
|
@ -213,11 +217,17 @@ body {
|
|||
var(--background-primary) 75%,
|
||||
hsl(0 0% 0%) 25%
|
||||
);
|
||||
--interactive-accent-disabled: color-mix(
|
||||
--interactive-accent-disabled-accent: color-mix(
|
||||
in hsl shorter hue,
|
||||
var(--background-modifier-border-hover) 50%,
|
||||
var(--interactive-accent) 40%
|
||||
);
|
||||
|
||||
--interactive-accent-disabled: color-mix(
|
||||
in hsl shorter hue,
|
||||
var(--background-modifier-border-hover) 50%,
|
||||
var(--background-primary) 40%
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
|
|
|
|||
10
Types/WebviewElement.ts
Normal file
10
Types/WebviewElement.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export interface WebviewElement extends Element {
|
||||
getURL(): string;
|
||||
isLoading(): boolean;
|
||||
capturePage(): Promise<NativeImage>
|
||||
executeJavaScript(code: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
export interface NativeImage {
|
||||
toDataURL(): string;
|
||||
}
|
||||
|
|
@ -305,8 +305,10 @@ export class VaultkeeperAISettingTab extends PluginSettingTab {
|
|||
private updateAllowUpdatingMemoriesSetting() {
|
||||
if (this.allowUpdatingMemoriesToggleComponent && this.allowUpdatingMemoriesSetting) {
|
||||
const enabled = this.settingsService.settings.enableMemories;
|
||||
const updateEnabled = this.settingsService.settings.allowUpdatingMemories;
|
||||
this.allowUpdatingMemoriesToggleComponent.disabled = !enabled;
|
||||
this.allowUpdatingMemoriesSetting.settingEl.toggleClass("setting-item-memories-disabled", !enabled);
|
||||
this.allowUpdatingMemoriesSetting.settingEl.toggleClass("setting-item-memories-disabled-accent", !enabled && updateEnabled);
|
||||
this.allowUpdatingMemoriesSetting.settingEl.toggleClass("setting-item-memories-disabled", !enabled && !updateEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue