@hashgraphonline/conversational-agent
Version:
Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org
495 lines (421 loc) โข 16.2 kB
text/typescript
/**
* Test script to verify that InscribeHashinalTool generates a form when attributes field is missing
*
* This script tests the end-to-end form generation functionality for the InscribeHashinalTool
* when required fields are missing from the input.
*
* Expected behavior:
* 1. Tool should be wrapped with FormValidatingToolWrapper (due to extendZodSchema)
* 2. When attributes field is missing, validation should fail
* 3. A form should be generated containing the missing attributes field
* 4. Tool should NOT execute directly but return form generation response
*
* Run with: pnpm tsx src/scripts/test-inscribe-form-generation.ts
*/
import dotenv from 'dotenv';
import { ConversationalAgent } from '../conversational-agent';
import type { ChatResponse } from '../base-agent';
import type { FormMessage } from '../forms/types';
import { NetworkType } from '@hashgraphonline/standards-sdk';
dotenv.config();
interface TestResult {
success: boolean;
message: string;
details?: Record<string, unknown>;
}
interface FormGenerationTestResult {
isFormGenerated: boolean;
hasAttributesField: boolean;
formMessage?: FormMessage;
originalResponse?: ChatResponse;
}
/**
* Validates that required environment variables are present
*/
function validateEnvironment(): TestResult {
const required = [
'HEDERA_OPERATOR_ID',
'HEDERA_OPERATOR_KEY',
'OPENAI_API_KEY'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
return {
success: false,
message: `Missing required environment variables: ${missing.join(', ')}`
};
}
return {
success: true,
message: 'Environment validation passed'
};
}
/**
* Creates and initializes a conversational agent for testing
*/
async function createTestAgent(): Promise<ConversationalAgent> {
const options = {
accountId: process.env.HEDERA_OPERATOR_ID!,
privateKey: process.env.HEDERA_OPERATOR_KEY!,
network: (process.env.HEDERA_NETWORK as 'testnet' | 'mainnet') || 'testnet',
openAIApiKey: process.env.OPENAI_API_KEY!,
openAIModelName: 'gpt-4o-mini',
verbose: true,
disableLogging: false,
entityMemoryEnabled: false,
};
const agent = new ConversationalAgent(options);
await agent.initialize();
return agent;
}
/**
* Test the FormValidatingToolWrapper directly by accessing the underlying tools
*/
async function testDirectToolAccess(agent: ConversationalAgent): Promise<FormGenerationTestResult> {
console.log('๐ง Testing direct tool access...');
try {
const underlyingAgent = agent.getAgent();
const tools = (underlyingAgent as unknown as { tools: Array<{ name: string; constructor: { name: string }; _call: (input: unknown) => Promise<string> }> }).tools;
console.log('Available tools:', tools.map(t => t.name));
const inscribeHashinalTool = tools.find(t => t.name === 'inscribeHashinal');
if (!inscribeHashinalTool) {
console.log('โ InscribeHashinal tool not found in tools array');
return {
isFormGenerated: false,
hasAttributesField: false
};
}
console.log('โ
Found InscribeHashinal tool:', {
name: inscribeHashinalTool.name,
type: inscribeHashinalTool.constructor.name,
isFormValidatingWrapper: inscribeHashinalTool.constructor.name === 'FormValidatingToolWrapper'
});
const incompleteInput = {
url: "https://example.com/test-image.png",
name: "Test NFT",
description: "A test NFT for form testing"
};
console.log('๐งช Calling tool directly with incomplete input...');
const result = await inscribeHashinalTool._call(incompleteInput);
console.log('๐ Direct tool call result:', {
resultType: typeof result,
resultLength: result?.length || 0,
resultPreview: result?.substring(0, 200) + '...'
});
try {
const parsed = JSON.parse(result);
if (parsed.requiresForm && parsed.formMessage) {
console.log('๐ Form generated by direct tool call!');
const formMessage = parsed.formMessage;
const formConfig = (formMessage as { formConfig?: { fields?: Array<{ name: string; type: string }> } }).formConfig;
const attributesField = formConfig?.fields?.find(
field => field.name === 'attributes'
);
const hasAttributesField = !!attributesField;
console.log('๐ Form analysis (direct call):', {
formId: (formMessage as { id?: string }).id,
toolName: (formMessage as { toolName?: string }).toolName,
fieldCount: formConfig?.fields?.length || 0,
hasAttributesField,
attributesFieldType: attributesField?.type,
fieldsPresent: formConfig?.fields?.map(f => f.name) || []
});
return {
isFormGenerated: true,
hasAttributesField,
formMessage,
};
}
} catch {
console.log('Result is not JSON or does not contain form data');
}
return {
isFormGenerated: false,
hasAttributesField: false
};
} catch (error) {
console.error('โ Error during direct tool test:', error);
throw error;
}
}
/**
* Tests that the InscribeHashinalTool is properly wrapped and generates forms for missing fields
*/
async function testInscribeFormGeneration(agent: ConversationalAgent): Promise<FormGenerationTestResult> {
console.log('๐งช Testing form generation for InscribeHashinalTool...');
const testMessage = `
I need to inscribe a hashinal NFT. Please call the inscribeHashinal tool with this data:
{
"url": "https://example.com/test-image.png",
"name": "Test NFT",
"description": "A test NFT for form testing"
}
Do not ask any questions, just call the tool now with those parameters.
`;
try {
console.log('๐ค Sending test message to agent...');
const response = await agent.processMessage(testMessage);
console.log('๐ Response received:', {
requiresForm: response.requiresForm,
hasFormMessage: !!response.formMessage,
outputLength: response.output?.length || 0,
toolCallCount: response.tool_calls?.length || 0,
hasError: !!response.error
});
if (response.tool_calls && response.tool_calls.length > 0) {
console.log('๐ง Tool calls made:',
response.tool_calls.map(call => ({
name: call.name,
hasArgs: !!call.args,
argKeys: call.args ? Object.keys(call.args) : [],
output: call.output?.substring(0, 100) + '...'
}))
);
}
if (response.tool_calls && response.tool_calls.length > 0) {
for (const toolCall of response.tool_calls) {
if (toolCall.name === 'inscribeHashinal' && toolCall.output) {
try {
const parsed = JSON.parse(toolCall.output);
if (parsed.requiresForm && parsed.formMessage) {
console.log('๐ Found form in tool call output!');
const formMessage = parsed.formMessage;
const formConfig = (formMessage as { formConfig?: { fields?: Array<{ name: string; type: string }> } }).formConfig;
const attributesField = formConfig?.fields?.find(
field => field.name === 'attributes'
);
const hasAttributesField = !!attributesField;
console.log('๐ Form analysis:', {
formId: (formMessage as { id?: string }).id,
toolName: (formMessage as { toolName?: string }).toolName,
fieldCount: formConfig?.fields?.length || 0,
hasAttributesField,
attributesFieldType: attributesField?.type,
fieldsPresent: formConfig?.fields?.map(f => f.name) || []
});
return {
isFormGenerated: true,
hasAttributesField,
formMessage,
originalResponse: response
};
}
} catch (parseError) {
console.log('Could not parse tool output as JSON:', parseError);
}
}
}
}
if (response.requiresForm && response.formMessage) {
const formMessage = response.formMessage;
const formConfig = (formMessage as { formConfig?: { fields?: Array<{ name: string; type: string }> } }).formConfig;
const attributesField = formConfig?.fields?.find(
field => field.name === 'attributes'
);
const hasAttributesField = !!attributesField;
console.log('๐ Form analysis (top-level):', {
formId: (formMessage as { id?: string }).id,
toolName: (formMessage as { toolName?: string }).toolName,
fieldCount: formConfig?.fields?.length || 0,
hasAttributesField,
attributesFieldType: attributesField?.type,
fieldsPresent: formConfig?.fields?.map(f => f.name) || []
});
return {
isFormGenerated: true,
hasAttributesField,
formMessage: formMessage as FormMessage,
originalResponse: response
};
} else {
console.log('โ ๏ธ No form was generated in response');
console.log('Response details:', {
output: response.output?.substring(0, 200) + '...',
toolCalls: response.tool_calls?.length || 0,
error: response.error
});
return {
isFormGenerated: false,
hasAttributesField: false,
originalResponse: response
};
}
} catch (error) {
console.error('โ Error during form generation test:', error);
throw error;
}
}
/**
* Verifies that the tool wrapper is working correctly by checking response structure
*/
function analyzeFormGenerationResponse(result: FormGenerationTestResult): TestResult {
if (!result.isFormGenerated) {
return {
success: false,
message: 'Form was not generated when attributes field was missing',
details: {
responseHadForm: result.isFormGenerated,
originalOutput: result.originalResponse?.output?.substring(0, 200)
}
};
}
if (!result.hasAttributesField) {
return {
success: false,
message: 'Form was generated but does not contain attributes field',
details: {
formFields: result.formMessage?.formConfig.fields.map(f => f.name) || [],
formId: result.formMessage?.id
}
};
}
if (result.formMessage?.toolName !== 'inscribeHashinal') {
return {
success: false,
message: 'Form was generated for wrong tool',
details: {
expectedTool: 'inscribeHashinal',
actualTool: result.formMessage?.toolName
}
};
}
return {
success: true,
message: 'Form generation test passed successfully',
details: {
formGenerated: true,
hasAttributesField: true,
toolName: result.formMessage?.toolName,
fieldCount: result.formMessage?.formConfig.fields.length,
formId: result.formMessage?.id
}
};
}
/**
* Validates that the form contains expected structure and required elements
*/
function validateFormStructure(formMessage: FormMessage): TestResult {
const issues: string[] = [];
if (!formMessage.id) {
issues.push('Form missing unique ID');
}
if (!formMessage.formConfig) {
issues.push('Form missing configuration');
}
if (!formMessage.formConfig.fields || formMessage.formConfig.fields.length === 0) {
issues.push('Form has no fields');
}
if (!formMessage.formConfig.title) {
issues.push('Form missing title');
}
const fieldNames = formMessage.formConfig.fields.map(f => f.name);
const essentialFields = ['attributes'];
const missingEssential = essentialFields.filter(field => !fieldNames.includes(field));
if (missingEssential.length > 0) {
issues.push(`Form missing essential fields: ${missingEssential.join(', ')}`);
}
const attributesField = formMessage.formConfig.fields.find(f => f.name === 'attributes');
if (attributesField) {
if (attributesField.type !== 'array') {
issues.push(`Attributes field has wrong type: ${attributesField.type}, expected: array`);
}
if (!attributesField.required) {
issues.push('Attributes field should be marked as required');
}
}
if (issues.length > 0) {
return {
success: false,
message: `Form structure validation failed: ${issues.join(', ')}`,
details: {
issues,
formStructure: {
id: formMessage.id,
title: formMessage.formConfig.title,
fieldCount: formMessage.formConfig.fields.length,
fields: formMessage.formConfig.fields.map(f => ({
name: f.name,
type: f.type,
required: f.required
}))
}
}
};
}
return {
success: true,
message: 'Form structure validation passed',
details: {
formId: formMessage.id,
title: formMessage.formConfig.title,
fieldCount: formMessage.formConfig.fields.length,
attributesFieldFound: true
}
};
}
/**
* Main test execution function
*/
async function runTest(): Promise<void> {
console.log('๐ Starting InscribeHashinalTool form generation test\n');
try {
console.log('๐ Step 1: Validating environment...');
const envResult = validateEnvironment();
if (!envResult.success) {
console.error('โ', envResult.message);
process.exit(1);
}
console.log('โ
', envResult.message, '\n');
console.log('๐ Step 2: Initializing conversational agent...');
const agent = await createTestAgent();
console.log('โ
Agent initialized successfully\n');
console.log('๐ Step 3a: Testing direct tool access...');
const directResult = await testDirectToolAccess(agent);
console.log('๐ Step 3b: Testing InscribeHashinalTool form generation via conversation...');
const formResult = directResult.isFormGenerated ? directResult : await testInscribeFormGeneration(agent);
console.log('๐ Step 4: Analyzing form generation results...');
const analysisResult = analyzeFormGenerationResponse(formResult);
if (!analysisResult.success) {
console.error('โ', analysisResult.message);
if (analysisResult.details) {
console.log('Details:', JSON.stringify(analysisResult.details, null, 2));
}
process.exit(1);
}
console.log('โ
', analysisResult.message);
if (formResult.formMessage) {
console.log('๐ Step 5: Validating form structure...');
const structureResult = validateFormStructure(formResult.formMessage);
if (!structureResult.success) {
console.error('โ', structureResult.message);
if (structureResult.details) {
console.log('Details:', JSON.stringify(structureResult.details, null, 2));
}
process.exit(1);
}
console.log('โ
', structureResult.message);
}
console.log('\n๐ All tests passed successfully!');
console.log('\n๐ Test Summary:');
console.log('โ
InscribeHashinalTool is properly wrapped with FormValidatingToolWrapper');
console.log('โ
Form is generated when attributes field is missing');
console.log('โ
Generated form contains attributes field for user input');
console.log('โ
Form structure is valid and complete');
if (analysisResult.details) {
console.log('\n๐ Final Details:');
console.log(` Form ID: ${analysisResult.details.formId}`);
console.log(` Tool Name: ${analysisResult.details.toolName}`);
console.log(` Field Count: ${analysisResult.details.fieldCount}`);
console.log(` Has Attributes Field: ${analysisResult.details.hasAttributesField}`);
}
await agent.cleanup();
} catch (error) {
console.error('โ Test failed with error:', error);
console.error('Error details:', error instanceof Error ? error.stack : String(error));
process.exit(1);
}
}
runTest().catch((error) => {
console.error('Unhandled test error:', error);
process.exit(1);
});