@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
JavaScript
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