UNPKG

@hashgraphonline/standards-agent-kit

Version:

A modular SDK for building on-chain autonomous agents using Hashgraph Online Standards, including HCS-10 for agent discovery and communication.

191 lines (170 loc) 8.44 kB
import { z } from 'zod'; import { BaseInscriberQueryTool } from './base-inscriber-tools'; import { InscriptionOptions } from '@hashgraphonline/standards-sdk'; import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; /** * Schema for inscribing from buffer */ const inscribeFromBufferSchema = z.object({ base64Data: z.string().min(1, 'Data cannot be empty').describe('The FULL content to inscribe as plain text or base64. When asked to "inscribe it" after retrieving content, pass ALL the content you retrieved here as plain text - NOT a summary, NOT URLs, NOT truncated. Example: if you fetched articles from any source, pass the complete article text you received.'), fileName: z.string().min(1, 'File name cannot be empty').describe('Name for the inscribed content. Required for all inscriptions.'), mimeType: z .string() .optional() .describe('MIME type of the content'), mode: z .enum(['file', 'hashinal']) .optional() .describe('Inscription mode: file or hashinal NFT'), metadata: z .record(z.unknown()) .optional() .describe('Metadata to attach to the inscription'), tags: z .array(z.string()) .optional() .describe('Tags to categorize the inscription'), chunkSize: z .number() .int() .positive() .optional() .describe('Chunk size for large files'), waitForConfirmation: z .boolean() .optional() .describe('Whether to wait for inscription confirmation'), timeoutMs: z .number() .int() .positive() .optional() .describe('Timeout in milliseconds for inscription (default: no timeout - waits until completion)'), apiKey: z .string() .optional() .describe('API key for inscription service'), }); /** * Tool for inscribing content from buffer */ export class InscribeFromBufferTool extends BaseInscriberQueryTool<typeof inscribeFromBufferSchema> { name = 'inscribeFromBuffer'; description = 'PRIMARY inscription tool for ANY content you have already retrieved. USE THIS when asked to "inscribe it" after fetching content from MCP tools, search results, or any text data. Pass THE FULL CONTENT as plain text in base64Data field - the tool accepts both plain text and base64. CRITICAL: Pass the COMPLETE content you retrieved, NOT a summary or truncated version. Example: If you fetched articles with full text, pass ALL that text here as plain text.'; get specificInputSchema() { return inscribeFromBufferSchema; } protected async executeQuery( params: z.infer<typeof inscribeFromBufferSchema>, _runManager?: CallbackManagerForToolRun ): Promise<unknown> { console.log(`[DEBUG] InscribeFromBufferTool.executeQuery called`); console.log(`[DEBUG] Buffer tool received base64Data length: ${params.base64Data?.length || 0}`); console.log(`[DEBUG] Buffer tool fileName: ${params.fileName}`); console.log(`[DEBUG] Buffer tool mimeType: ${params.mimeType}`); if (!params.base64Data || params.base64Data.trim() === '') { console.log(`[InscribeFromBufferTool] ERROR: No data provided`); throw new Error('No data provided. Cannot inscribe empty content. Please provide valid base64 encoded data.'); } if (!params.fileName || params.fileName.trim() === '') { console.log(`[InscribeFromBufferTool] ERROR: No fileName provided`); throw new Error('No fileName provided. A valid fileName is required for inscription.'); } let buffer: Buffer; // First check if it's valid base64 const isValidBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(params.base64Data); if (isValidBase64) { // Try to decode as base64 try { buffer = Buffer.from(params.base64Data, 'base64'); console.log(`[InscribeFromBufferTool] Successfully decoded base64 data`); } catch (error) { console.log(`[InscribeFromBufferTool] ERROR: Failed to decode base64`); throw new Error('Failed to decode base64 data. Please ensure the data is properly encoded.'); } } else { // Not base64, treat as plain text and encode it console.log(`[InscribeFromBufferTool] WARNING: Data is not base64 encoded, treating as plain text`); buffer = Buffer.from(params.base64Data, 'utf8'); console.log(`[InscribeFromBufferTool] Converted plain text to buffer`); } console.log(`[InscribeFromBufferTool] Buffer length after conversion: ${buffer.length}`); if (buffer.length === 0) { console.log(`[InscribeFromBufferTool] ERROR: Buffer is empty after conversion`); throw new Error('Buffer is empty after conversion. The provided data appears to be invalid or empty.'); } if (buffer.length < 10) { console.log(`[InscribeFromBufferTool] WARNING: Buffer is very small (${buffer.length} bytes)`); console.log(`[InscribeFromBufferTool] Buffer content preview: ${buffer.toString('utf8', 0, Math.min(buffer.length, 50))}`); throw new Error(`Buffer content is too small (${buffer.length} bytes). This may indicate empty or invalid content. Please verify the source data contains actual content.`); } if (buffer.toString('utf8', 0, Math.min(buffer.length, 100)).trim() === '') { console.log(`[InscribeFromBufferTool] ERROR: Buffer contains only whitespace or empty content`); throw new Error('Buffer contains only whitespace or empty content. Cannot inscribe meaningless data.'); } // Check for empty HTML pattern (just links with no content) const contentStr = buffer.toString('utf8'); const emptyHtmlPattern = /<a\s+href=["'][^"']+["']\s*>\s*<\/a>/i; const hasOnlyEmptyLinks = emptyHtmlPattern.test(contentStr) && contentStr.replace(/<[^>]+>/g, '').trim().length < 50; if (hasOnlyEmptyLinks) { console.log(`[InscribeFromBufferTool] ERROR: Buffer contains empty HTML with just links and no content`); throw new Error('Buffer contains empty HTML with only links and no actual content. When inscribing content from external sources, use the actual article text you retrieved, not empty HTML with links.'); } const options: InscriptionOptions = { mode: params.mode, metadata: params.metadata, tags: params.tags, chunkSize: params.chunkSize, waitForConfirmation: params.waitForConfirmation ?? true, waitMaxAttempts: 10, waitIntervalMs: 3000, apiKey: params.apiKey, network: this.inscriberBuilder['hederaKit'].client.network.toString().includes('mainnet') ? 'mainnet' : 'testnet', }; try { let result: Awaited<ReturnType<typeof this.inscriberBuilder.inscribe>>; if (params.timeoutMs) { const timeoutPromise = new Promise<never>((_, reject) => { setTimeout( () => reject(new Error(`Inscription timed out after ${params.timeoutMs}ms`)), params.timeoutMs ); }); result = await Promise.race([ this.inscriberBuilder.inscribe( { type: 'buffer', buffer, fileName: params.fileName, mimeType: params.mimeType, }, options ), timeoutPromise, ]); } else { result = await this.inscriberBuilder.inscribe( { type: 'buffer', buffer, fileName: params.fileName, mimeType: params.mimeType, }, options ); } if (result.confirmed) { const topicId = result.inscription?.topic_id || result.result.topicId; const network = options.network || 'testnet'; const cdnUrl = topicId ? `https://kiloscribe.com/api/inscription-cdn/${topicId}?network=${network}` : null; return `Successfully inscribed and confirmed content on the Hedera network!\n\nTransaction ID: ${result.result.transactionId}\nTopic ID: ${topicId || 'N/A'}${cdnUrl ? `\nView inscription: ${cdnUrl}` : ''}\n\nThe inscription is now available.`; } else { return `Successfully submitted inscription to the Hedera network!\n\nTransaction ID: ${result.result.transactionId}\n\nThe inscription is processing and will be confirmed shortly.`; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to inscribe from buffer'; throw new Error(`Inscription failed: ${errorMessage}`); } } }