UNPKG

@hashgraphonline/conversational-agent

Version:

Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org

252 lines (251 loc) 8.65 kB
import { StructuredTool } from "@langchain/core/tools"; import { z } from "zod"; import { Logger } from "@hashgraphonline/standards-sdk"; import { isFormValidatable } from "@hashgraphonline/standards-agent-kit"; class FormValidatingToolWrapper extends StructuredTool { constructor(originalTool, formGenerator, config = {}) { super(); this.originalTool = originalTool; this.formGenerator = formGenerator; this.validationConfig = config; this.logger = new Logger({ module: "FormValidatingToolWrapper" }); this.name = originalTool.name; this.description = originalTool.description; this.schema = originalTool.schema; this.logger.info(`🔧 FormValidatingToolWrapper created for tool: ${this.name}`, { originalToolName: originalTool.name, originalToolType: originalTool.constructor.name, wrapperType: this.constructor.name }); } /** * Validate the input against the schema */ validateInput(input) { try { this.schema.parse(input); return { isValid: true }; } catch (error) { if (error instanceof z.ZodError) { const errors = error.errors.filter((err) => { const fieldName = err.path[0]; return !this.validationConfig.skipFields?.includes(fieldName); }).map((err) => `${err.path.join(".")}: ${err.message}`); return { isValid: false, errors }; } return { isValid: false, errors: ["Validation failed"] }; } } /** * Gets the shape keys from the schema if it's a ZodObject */ getSchemaShape() { if (this.isZodObject(this.schema)) { return Object.keys(this.schema.shape); } return []; } /** * Executes the wrapped tool's original implementation directly, bypassing wrapper logic. */ async executeOriginal(input, runManager) { const tool = this.originalTool; if ("_call" in tool && typeof tool._call === "function") { return tool._call(input, runManager); } if ("call" in tool && typeof tool.call === "function") { return tool.call(input, runManager); } throw new Error("Original tool has no callable implementation"); } /** * Provides access to the wrapped tool instance for executors that want to bypass the wrapper. */ getOriginalTool() { return this.originalTool; } /** * Checks if tool implements FormValidatable method */ hasFormValidatableMethod(tool, methodName) { return tool !== null && typeof tool === "object" && methodName in tool && typeof tool[methodName] === "function"; } /** * Expose FormValidatable methods by delegating to the underlying tool when available. */ getFormSchema() { if (this.hasFormValidatableMethod(this.originalTool, "getFormSchema")) { return this.originalTool.getFormSchema(); } return this.schema; } getEssentialFields() { if (this.hasFormValidatableMethod(this.originalTool, "getEssentialFields")) { return this.originalTool.getEssentialFields(); } return []; } isFieldEmpty(fieldName, value) { if (this.hasFormValidatableMethod(this.originalTool, "isFieldEmpty")) { return this.originalTool.isFieldEmpty(fieldName, value); } if (value === void 0 || value === null || value === "") { return true; } if (Array.isArray(value) && value.length === 0) { return true; } return false; } /** * Calculates which fields are missing from the input */ calculateMissingFields(input, isCustom) { const missingFields = /* @__PURE__ */ new Set(); if (!isCustom) { return missingFields; } const essentialFields = this.getEssentialFields(); for (const fieldName of essentialFields) { const value = input[fieldName]; if (this.isFieldEmpty(fieldName, value)) { missingFields.add(fieldName); } } return missingFields; } /** * Creates a form message with optional JSON schema */ async createFormMessage(schema, input, missingFields) { let formMessage = await this.formGenerator.generateFormFromSchema( schema, input, { toolName: this.name, toolDescription: this.description }, missingFields ); if (this.isZodObject(schema)) { try { const { jsonSchema, uiSchema } = this.formGenerator.generateJsonSchemaForm( schema, input, missingFields ); formMessage = { ...formMessage, jsonSchema, uiSchema }; } catch (error) { this.logger.warn("Failed to generate JSON Schema for RJSF:", error); } } formMessage.partialInput = input; return formMessage; } /** * Type guard to check if a schema is a ZodObject */ isZodObject(schema) { const def = schema._def; return !!(def && def.typeName === "ZodObject"); } /** * Check if we should generate a form for this tool invocation */ shouldGenerateForm(input) { this.logger.info(`shouldGenerateForm called for ${this.name}/${this.originalTool.name}`, { input, hasCustomValidation: !!this.validationConfig.customValidation }); if (this.validationConfig.customValidation) { const result = !this.validationConfig.customValidation(input); this.logger.info(`Custom validation result: ${result}`); return result; } if (isFormValidatable(this.originalTool)) { this.logger.info(`Tool ${this.originalTool.name} implements FormValidatable, using custom logic`); return this.originalTool.shouldGenerateForm(input); } this.logger.info(`Tool ${this.originalTool.name} using schema validation only`); const validation = this.validateInput(input); this.logger.info(`Schema validation for ${this.originalTool.name}:`, { isValid: validation.isValid, errors: validation.errors }); return !validation.isValid; } /** * Checks if input has bypass flags that skip form generation */ hasFormBypassFlags(input) { return input.__fromForm === true || input.renderForm === false; } /** * Override _call to intercept tool execution */ async _call(input, runManager) { this.logger.info(`🚨🚨🚨 FormValidatingToolWrapper._call INTERCEPTING ${this.name} 🚨🚨🚨`, { input, inputKeys: Object.keys(input), schemaShape: this.getSchemaShape(), stackTrace: new Error().stack?.split("\n").slice(0, 5) }); const inputRecord = input; if (this.hasFormBypassFlags(inputRecord)) { this.logger.info("Bypassing form generation and executing original tool due to submission flags"); return this.executeOriginal(inputRecord, runManager); } const shouldGenerate = this.shouldGenerateForm(input); this.logger.info(`FormValidatingToolWrapper decision for ${this.name}:`, { shouldGenerateForm: shouldGenerate, toolName: this.name, originalToolName: this.originalTool.name }); if (shouldGenerate) { this.logger.info(`Generating form for incomplete input in ${this.name}`); try { const isCustom = isFormValidatable(this.originalTool); const schemaToUse = isCustom ? this.getFormSchema() : this.schema; const missingFields = this.calculateMissingFields( input, isCustom ); const schemaFields = this.isZodObject(schemaToUse) ? Object.keys(schemaToUse.shape) : []; this.logger.info(`Using ${isCustom ? "CUSTOM" : "DEFAULT"} schema for form generation`, { toolName: this.originalTool.name, schemaType: schemaToUse.constructor?.name, schemaFields, isCustomSchema: isCustom }); const formMessage = await this.createFormMessage( schemaToUse, input, missingFields ); const result = { requiresForm: true, formMessage, message: `Please complete the form to provide the required information for ${this.name}.` }; this.logger.info(`FormValidatingToolWrapper returning form result for ${this.name}`); return JSON.stringify(result); } catch (error) { this.logger.error("Failed to generate form:", error); } } this.logger.info(`FormValidatingToolWrapper passing through to original tool ${this.name}`); return this.executeOriginal(input, runManager); } } function wrapToolWithFormValidation(tool, formGenerator, config = {}) { return new FormValidatingToolWrapper(tool, formGenerator, config); } export { FormValidatingToolWrapper, wrapToolWithFormValidation }; //# sourceMappingURL=index33.js.map