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

495 lines (421 loc) โ€ข 16.2 kB
/** * 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); });