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

687 lines (686 loc) 23.1 kB
import { zodToJsonSchema } from "zod-to-json-schema"; import { extractRenderConfigs, generateFieldOrdering } from "@hashgraphonline/standards-agent-kit"; import { Logger } from "@hashgraphonline/standards-sdk"; import { fieldTypeRegistry } from "./index13.js"; import { fieldGuidanceRegistry } from "./index14.js"; import "./index43.js"; import { FIELD_PRIORITIES } from "./index44.js"; function isZodObjectSchema(schema) { return typeof schema === "object" && schema !== null && "shape" in schema; } class FormGenerator { constructor() { this.logger = new Logger({ module: "FormGenerator" }); } /** * Creates a form message from a Zod validation error * @param error The Zod validation error * @param schema The original Zod schema * @param toolName Name of the tool that failed validation * @param originalPrompt The user's original request * @returns FormMessage to send to the chat UI */ generateFormFromError(error, schema, toolName, originalPrompt) { const validationErrors = this.extractValidationErrors(error); const missingFields = this.identifyMissingFields(validationErrors, schema); const formConfig = this.createFormConfig(schema, missingFields, toolName); return { type: "form", id: `form_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, formConfig, originalPrompt, toolName, validationErrors }; } /** * Generates a form from a schema and partial input * @param schema The Zod schema to generate a form from * @param partialInput Any partial input already provided * @param context Additional context about the tool * @param preCalculatedMissingFields Optional pre-calculated missing fields set. If undefined, includes all fields from schema. * @returns FormMessage to send to the chat UI */ async generateFormFromSchema(schema, partialInput, context, preCalculatedMissingFields) { let missingFields; this.logger.info(`🏁 FormGenerator.generateFormFromSchema starting`, { toolName: context.toolName, partialInput, hasSchema: !!schema, hasShape: !!(schema && isZodObjectSchema(schema)), hasPreCalculatedFields: preCalculatedMissingFields !== void 0, preCalculatedFieldsSize: preCalculatedMissingFields?.size || 0 }); if (preCalculatedMissingFields !== void 0) { missingFields = preCalculatedMissingFields; this.logger.info(`📋 Using pre-calculated missing fields`, { missingFieldsCount: missingFields.size, missingFields: Array.from(missingFields) }); } else { missingFields = /* @__PURE__ */ new Set(); const zodObject = this.extractZodObject(schema); if (zodObject) { const shape = zodObject.shape; for (const fieldName of Object.keys(shape)) { missingFields.add(fieldName); this.logger.info( `⭐ Including all fields from focused schema: ${fieldName}` ); } } this.logger.info(`📋 Using ALL fields from focused schema`, { totalFields: zodObject ? Object.keys(zodObject.shape).length : 0, missingFieldsCount: missingFields.size, missingFields: Array.from(missingFields) }); } const formConfig = this.createFormConfig( schema, missingFields, context.toolName, preCalculatedMissingFields ); return { type: "form", id: `form_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, formConfig, originalPrompt: context.toolDescription || `Complete ${context.toolName}`, toolName: context.toolName, validationErrors: [] }; } /** * Extracts validation errors from ZodError */ extractValidationErrors(error) { return error.issues.map((issue) => ({ path: issue.path.map((p) => String(p)), message: issue.message, code: issue.code })); } /** * Identifies which fields are missing or invalid from validation errors */ identifyMissingFields(errors, _schema) { const missingFields = /* @__PURE__ */ new Set(); errors.forEach((error) => { const fieldPath = error.path.join("."); if (fieldPath) { missingFields.add(fieldPath); } }); return missingFields; } /** * Creates form configuration from schema */ createFormConfig(schema, missingFields, toolName, preCalculatedMissingFields) { const extractedConfig = this.extractRenderConfigsSafely(schema); const fieldOrdering = this.generateFieldOrderingSafely(schema); const fields = this.generateFormFields( schema, extractedConfig, missingFields, fieldOrdering, preCalculatedMissingFields, toolName ); const globalGuidance = fieldGuidanceRegistry.getGlobalGuidance(toolName); let description = this.generateFormDescription( toolName, missingFields.size ); if (globalGuidance?.qualityStandards) { description += "\n\nQuality Guidelines:\n" + globalGuidance.qualityStandards.map((standard) => `• ${standard}`).join("\n"); } return { title: this.generateFormTitle(toolName), description, fields, submitLabel: "Continue", cancelLabel: "Cancel", metadata: { toolName, missingFieldCount: missingFields.size, globalGuidance } }; } /** * Safely extracts render configs from schema */ extractRenderConfigsSafely(schema) { try { if (typeof extractRenderConfigs === "function") { return extractRenderConfigs(schema); } } catch (error) { this.logger.warn("Could not extract render configs:", error); } return { fields: {}, groups: {}, order: [], metadata: {} }; } /** * Safely generates field ordering from schema */ generateFieldOrderingSafely(schema) { try { if (typeof generateFieldOrdering === "function") { const ordering = generateFieldOrdering( schema ); const sections = Object.values(ordering.sections).map((section) => ({ fields: section.fields })); return { sections }; } } catch (error) { this.logger.warn("Could not generate field ordering:", error); } return { sections: [] }; } /** * Determines field priority for progressive disclosure */ getFieldPriority(name, renderConfig, isRequired) { if (renderConfig?.ui?.priority) { const priority = renderConfig.ui.priority; if (Object.values(FIELD_PRIORITIES).includes(priority)) { return priority; } } if (isRequired === true) { return FIELD_PRIORITIES.ESSENTIAL; } const ui = renderConfig?.ui; if (ui?.advanced === true) { return FIELD_PRIORITIES.ADVANCED; } if (ui?.expert === true) { return FIELD_PRIORITIES.EXPERT; } return FIELD_PRIORITIES.COMMON; } /** * Determines which fields should be included in the form */ determineFieldsToInclude(schema, missingFields, preCalculatedMissingFields) { const fieldsToInclude = /* @__PURE__ */ new Set(); if (preCalculatedMissingFields === void 0) { this.logger.info( `⭐ Focused schema mode - including ALL fields from schema` ); const allSchemaFields = this.extractFieldsFromSchema(schema); allSchemaFields.forEach((fieldName) => { fieldsToInclude.add(fieldName); this.logger.info(`✅ Including focused schema field: ${fieldName}`); }); } else if (preCalculatedMissingFields.size > 0) { this.logger.info( `📋 Using ONLY pre-calculated missing fields (${preCalculatedMissingFields.size} fields)`, { fields: Array.from(preCalculatedMissingFields) } ); preCalculatedMissingFields.forEach((fieldName) => { fieldsToInclude.add(fieldName); this.logger.info(`✅ Including pre-calculated field: ${fieldName}`); }); } else { this.logger.info( "⚠️ No pre-calculated fields, falling back to schema analysis" ); this.includeRequiredMissingFields(schema, missingFields, fieldsToInclude); } return fieldsToInclude; } /** * Includes required fields that are missing */ includeRequiredMissingFields(schema, missingFields, fieldsToInclude) { const allSchemaFields = this.extractFieldsFromSchema(schema); allSchemaFields.forEach((fieldName) => { const isRequired = this.isFieldRequired(schema, fieldName); const isMissing = missingFields.has(fieldName); const shouldInclude = isMissing && isRequired; this.logger.info(`🔍 FormGenerator field inclusion check: ${fieldName}`, { isRequired, isMissing, shouldInclude }); if (shouldInclude) { fieldsToInclude.add(fieldName); } }); } /** * Creates form fields from ordered field names */ createOrderedFields(fieldsToInclude, fieldOrdering, extractedConfig, schema, toolName) { const fields = []; const processedFields = /* @__PURE__ */ new Set(); if (fieldOrdering.sections.length > 0) { const orderedFieldNames = fieldOrdering.sections.flatMap((s) => s.fields); orderedFieldNames.forEach((fieldName) => { if (fieldsToInclude.has(fieldName) && !processedFields.has(fieldName)) { const field = this.createFormField( fieldName, extractedConfig.fields[fieldName], schema, fieldName, toolName ); if (field) { fields.push(field); processedFields.add(fieldName); } } }); } fieldsToInclude.forEach((fieldName) => { if (!processedFields.has(fieldName)) { const field = this.createFormField( fieldName, extractedConfig.fields[fieldName], schema, fieldName, toolName ); if (field) { fields.push(field); } } }); return fields; } /** * Generates form fields from schema and validation errors */ generateFormFields(schema, extractedConfig, missingFields, fieldOrdering, preCalculatedMissingFields, toolName) { const fieldsToInclude = this.determineFieldsToInclude( schema, missingFields, preCalculatedMissingFields ); let fields = this.createOrderedFields( fieldsToInclude, fieldOrdering, extractedConfig, schema, toolName ); if (fields.length === 0 && missingFields.size > 0) { fields = Array.from(missingFields).map( (fieldName) => this.createFormField( fieldName, extractedConfig.fields[fieldName], schema, fieldName, toolName ) ).filter( (field) => field !== null && field !== void 0 ); } return fields; } /** * Creates a single form field */ createFormField(fieldName, renderConfig, schema, fieldPath, toolName) { const type = this.mapFieldType(renderConfig?.fieldType, schema, fieldPath); const isRequired = this.isFieldRequired(schema, fieldPath || fieldName); const guidance = toolName ? fieldGuidanceRegistry.getFieldGuidance(toolName, fieldName) : null; const finalType = guidance?.fieldTypeOverride || type; const field = { name: fieldName, label: renderConfig?.ui?.label || this.humanizeFieldName(fieldName), type: finalType, required: isRequired, priority: this.getFieldPriority(fieldName, renderConfig, isRequired) }; if (guidance) { if (guidance.suggestions && guidance.suggestions.length > 0) { field.suggestions = guidance.suggestions; if (!field.placeholder) { field.placeholder = `e.g., ${guidance.suggestions[0]}`; } } if (guidance.predefinedOptions) { field.options = [ ...field.options || [], ...guidance.predefinedOptions ]; } if (guidance.contextualHelpText) { field.helpText = guidance.contextualHelpText; } if (guidance.warnings) { field.warnings = guidance.warnings.map((w) => w.message); } if (guidance.validationRules) { const { qualityChecks } = guidance.validationRules; if (qualityChecks) { field.contextualGuidance = { qualityStandards: [], examples: guidance.suggestions || [], avoidPatterns: qualityChecks.forbidTechnicalTerms || [] }; if (qualityChecks.minNonTechnicalWords) { field.contextualGuidance.qualityStandards?.push( `Use at least ${qualityChecks.minNonTechnicalWords} meaningful words` ); } if (qualityChecks.forbidTechnicalTerms) { field.contextualGuidance.qualityStandards?.push( `Avoid technical terms like: ${qualityChecks.forbidTechnicalTerms.join( ", " )}` ); } } } } if (renderConfig) { field.renderConfig = renderConfig; } if (renderConfig?.ui?.placeholder) { field.placeholder = renderConfig.ui.placeholder; } if (renderConfig?.ui?.helpText) { field.helpText = renderConfig.ui.helpText; } if (renderConfig?.constraints) { const validation = {}; if (renderConfig.constraints.min !== void 0) validation.min = renderConfig.constraints.min; if (renderConfig.constraints.max !== void 0) validation.max = renderConfig.constraints.max; if (renderConfig.constraints.minLength !== void 0) validation.minLength = renderConfig.constraints.minLength; if (renderConfig.constraints.maxLength !== void 0) validation.maxLength = renderConfig.constraints.maxLength; if (renderConfig.constraints.pattern !== void 0) validation.pattern = renderConfig.constraints.pattern; if (Object.keys(validation).length > 0) { field.validation = validation; } } if (renderConfig?.options) { field.options = renderConfig.options.map((opt) => ({ value: String(opt.value), label: opt.label, ...opt.disabled !== void 0 && { disabled: opt.disabled } })); } return field; } /** * Maps render config field type to form field type with fallback inference */ mapFieldType(fieldType, schema, fieldPath) { if (!fieldType && schema && fieldPath) { const inferredType = this.inferTypeFromSchema(schema, fieldPath); if (inferredType) { return inferredType; } } if (!fieldType && fieldPath) { const registryType = fieldTypeRegistry.detectType(fieldPath); if (registryType) { return registryType; } } if (!fieldType) { return "text"; } const normalizedType = fieldType.toLowerCase(); if (["text", "string"].includes(normalizedType)) return "text"; if (["number", "integer", "float", "decimal"].includes(normalizedType)) return "number"; if (["select", "enum", "dropdown"].includes(normalizedType)) return "select"; if (["checkbox", "boolean", "bool"].includes(normalizedType)) return "checkbox"; if (["textarea", "longtext", "multiline"].includes(normalizedType)) return "textarea"; if (["file", "upload", "attachment"].includes(normalizedType)) return "file"; if (["array", "list"].includes(normalizedType)) return "array"; if (["object", "json"].includes(normalizedType)) return "object"; if (["currency", "money", "price"].includes(normalizedType)) return "currency"; if (["percentage", "percent"].includes(normalizedType)) return "percentage"; return "text"; } /** * Converts field name to human-readable label */ humanizeFieldName(fieldName) { return fieldName.replace(/([A-Z])/g, " $1").replace(/_/g, " ").replace(/\./g, " ").trim().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" "); } /** * Generates a title for the form */ generateFormTitle(toolName) { const cleanName = toolName.replace(/Tool$/, "").replace(/Hedera/g, "").replace(/([A-Z])/g, " $1").trim(); return `Complete ${cleanName} Information`; } /** * Safely extracts ZodObject from a schema, returns null if not an object schema */ extractZodObject(schema) { try { const def = schema._def; if (def && def.typeName === "ZodObject") { return schema; } } catch (error) { this.logger.debug("Could not extract ZodObject from schema:", error); } return null; } /** * Extracts field names from Zod schema structure */ extractFieldsFromSchema(schema) { const fields = []; const zodObject = this.extractZodObject(schema); if (zodObject) { fields.push(...Object.keys(zodObject.shape)); return fields; } try { const def = schema._def; if (def && def.typeName === "ZodUnion" && def.options) { const firstOption = def.options[0]; const firstOptionObject = this.extractZodObject(firstOption); if (firstOptionObject) { fields.push(...Object.keys(firstOptionObject.shape)); } } } catch (error) { this.logger.debug( "Could not extract fields from schema structure:", error ); } return fields; } /** * Infers field type from Zod schema */ inferTypeFromSchema(schema, fieldPath) { try { const zodObject = this.extractZodObject(schema); if (!zodObject) return null; const shape = zodObject.shape; if (!shape) return null; let fieldSchema = shape[fieldPath]; if (!fieldSchema) return null; const fieldDef = fieldSchema._def; if (fieldDef && fieldDef.typeName === "ZodOptional" && fieldDef.innerType) { fieldSchema = fieldDef.innerType; } if (!fieldSchema || !fieldSchema._def) return null; const typeDef = fieldSchema._def; const fieldTypeName = typeDef.typeName; const lowerPath = fieldPath.toLowerCase(); switch (fieldTypeName) { case "ZodString": if (lowerPath.includes("memo") || lowerPath.includes("description")) { return "textarea"; } return "text"; case "ZodNumber": if (lowerPath.includes("percent")) { return "percentage"; } if (lowerPath.includes("price") || lowerPath.includes("cost")) { return "currency"; } return "number"; case "ZodBoolean": return "checkbox"; case "ZodEnum": case "ZodNativeEnum": return "select"; case "ZodArray": return "array"; case "ZodObject": return "object"; default: return "text"; } } catch (error) { this.logger.debug("Could not infer type from schema:", error); } return null; } /** * Determines if a field is required based on the Zod schema */ isFieldRequired(schema, fieldPath) { if (!schema || !fieldPath) { return false; } try { const zodObject = this.extractZodObject(schema); if (!zodObject) return false; const shape = zodObject.shape; if (!shape || !shape[fieldPath]) return false; const fieldSchema = shape[fieldPath]; if (!fieldSchema || !fieldSchema._def) return true; const fieldDef = fieldSchema._def; const typeName = fieldDef.typeName; if (typeName === "ZodOptional") { return false; } if (typeName === "ZodDefault") { return false; } if (fieldDef.defaultValue !== void 0) { return false; } return true; } catch (error) { this.logger.debug( `Could not determine if field ${fieldPath} is required:`, error ); } return false; } /** * Generates a description for the form */ generateFormDescription(toolName, fieldCount) { if (fieldCount === 0) { return "Please provide the required information to continue with your request."; } return `Please provide the following ${fieldCount} required field${fieldCount !== 1 ? "s" : ""} to continue with your request.`; } /** * Generates JSON Schema and uiSchema from a Zod schema for use with @rjsf/core * @param zodSchema The Zod schema to convert * @param partialInput Existing input data to filter out fields that already have values * @param missingFields Set of fields that are missing and should be shown * @returns Object containing jsonSchema and uiSchema */ generateJsonSchemaForm(zodSchema, partialInput, missingFields) { const fullJsonSchema = zodToJsonSchema(zodSchema, { target: "jsonSchema7" }); const uiSchema = {}; let jsonSchema = fullJsonSchema; if (missingFields && missingFields.size > 0) { const fullSchemaAsObject = fullJsonSchema; if (fullSchemaAsObject.properties && typeof fullSchemaAsObject.properties === "object") { const filteredSchema = { ...fullSchemaAsObject, type: "object", properties: {}, required: [] }; let fieldsAdded = 0; missingFields.forEach((fieldName) => { if (fullSchemaAsObject.properties && fullSchemaAsObject.properties[fieldName]) { filteredSchema.properties[fieldName] = fullSchemaAsObject.properties[fieldName]; fieldsAdded++; } }); if (Array.isArray(fullSchemaAsObject.required)) { filteredSchema.required = fullSchemaAsObject.required.filter( (field) => missingFields.has(field) ); } if (fieldsAdded > 0) { jsonSchema = filteredSchema; } } } const fieldNames = this.extractFieldsFromSchema(zodSchema); fieldNames.forEach((fieldName) => { const isRequired = this.isFieldRequired(zodSchema, fieldName); const priority = this.getFieldPriority(fieldName, void 0, isRequired); const lower = fieldName.toLowerCase(); if (lower === "attributes" || lower === "metadata" || lower === "properties") { uiSchema[fieldName] = { "ui:options": { collapsible: true, collapsed: true } }; } switch (priority) { case "essential": if (isRequired) { uiSchema[fieldName] = { ...uiSchema[fieldName], "ui:help": "Required field" }; } break; case "advanced": case "expert": uiSchema[fieldName] = { ...uiSchema[fieldName], "ui:options": { ...uiSchema[fieldName]?.["ui:options"], collapsed: true } }; break; } }); return { jsonSchema, uiSchema }; } } export { FormGenerator }; //# sourceMappingURL=index11.js.map