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