UNPKG

ttc-ai-client

Version:

TypeScript client sdk for TTC AI services with decorators and schema validation.

443 lines 17.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.ttc = void 0; require("reflect-metadata"); const typedi_1 = __importDefault(require("typedi")); const zod_1 = require("zod"); const ttcAI_1 = require("./ttcAI"); // Choose appropriate EventEmitter for runtime (Node or Browser) const eventEmitter_1 = require("./utils/eventEmitter"); const ttc_server_1 = require("./ttc_server"); const zodToTs_1 = require("./utils/zodToTs"); const EventEmitter = (0, eventEmitter_1.getEventEmitterClass)(); const zodToJsonSchema = zod_1.z.toJSONSchema; class ttc { // Helper function to convert TypeScript types to Zod schemas static createZodSchemaFromParams(paramNames, paramTypes) { if (paramNames.length === 0) { return zod_1.z.object({}); } const schemaFields = {}; paramNames.forEach((name, index) => { const type = paramTypes[index]; const typeName = type?.name?.toLowerCase() || 'unknown'; switch (typeName) { case 'string': schemaFields[name] = zod_1.z.string(); break; case 'number': schemaFields[name] = zod_1.z.number(); break; case 'boolean': schemaFields[name] = zod_1.z.boolean(); break; case 'array': schemaFields[name] = zod_1.z.array(zod_1.z.any()); break; case 'object': schemaFields[name] = zod_1.z.object({}).passthrough(); break; case 'date': schemaFields[name] = zod_1.z.date(); break; default: // For unknown types, use z.any() schemaFields[name] = zod_1.z.any(); break; } }); return zod_1.z.object(schemaFields); } // Helper function to extract type names from Zod schemas static getZodTypeName(properties) { const params = {}; for (const key in properties) { if (properties.hasOwnProperty(key)) { if (properties[key].type === 'object' && properties[key].properties) { params[key] = _a.getZodTypeName(properties[key].properties); } else { params[key] = properties[key].type || "any"; } } } return params; } static instance(name) { const finalName = typeof name === 'string' ? name : name.name; const cls = typedi_1.default.get(finalName); if (!cls) { return null; } return cls; } static requestContext(args) { const context = args[args.length - 1]; if (!context) { return {}; } return { socket: context.socket, _scid: context._scid }; } static async invoke(method, args, requestConfig) { const parts = method.split("."); // console.log(parts, 'Method parts'); if (parts.length === 3) { // this is an origin function return await this.invoke_origin(parts, { function: method, arguments: args }, requestConfig._scid); } const [className, methodName] = parts; // let auth_data = null; if (!typedi_1.default.has(className)) { // throw new Error(`Service ${className} not found`); return { status: "error", data: `${className} does not exist in api`, }; } const serviceInstance = typedi_1.default.get(className); if (!serviceInstance[methodName]) { return { status: "error", data: `function ${methodName} does not exist in ${className} module`, }; } const funcRegistry = this.rpcRegistry[method]; // Ensure args is an array if (!Array.isArray(args)) { args = args ? [args] : []; } // Validate input using schema if available if (funcRegistry.validate && funcRegistry.inputSchema && args.length > 0) { try { // For single parameter methods, validate the first argument directly if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) { const validatedArgs = funcRegistry.inputSchema.parse(args[0]); args[0] = validatedArgs; } else if (args.length > 1) { // For multiple parameters, create an object from parameter names and validate const paramNames = Object.keys(funcRegistry.params); const paramObj = {}; paramNames.forEach((name, index) => { if (index < args.length) { paramObj[name] = args[index]; } }); const validatedObj = funcRegistry.inputSchema.parse(paramObj); // Update args array with validated values paramNames.forEach((name, index) => { if (index < args.length) { args[index] = validatedObj[name]; } }); } } catch (validationError) { return { status: 'error', data: `Validation error: ${validationError.message}` }; } } // console.log(args, 'VALIDATED INPUT ARGS') args[funcRegistry.param_index] = { socket: requestConfig.socket }; // const args_ = try { const response = await serviceInstance[methodName](...args); if (response) { return { status: 'success', data: response }; } } catch (error) { return { status: 'error', data: error.message }; } } static init(config) { const { modules, socketCb, publickKey, app_id } = config; this.app_id = app_id; if (!app_id) { throw new Error("app_id is required"); } if (!publickKey) { throw new Error("publickKey is required"); } this.socketCb = socketCb; this.publickKey = publickKey; // instantiate emitter appropriate for environment _a.event = (0, eventEmitter_1.createEmitter)(); _a.ai = new ttcAI_1.ttcAI(); this.server = new ttc_server_1.RPCClient(ttcAI_1.ttcAI.ttc_Uri, async () => { const apikey = this.publickKey || ''; return apikey; }, async (socket) => { this.handleSocket(socket); if (socketCb) await socketCb(socket); }); _a.ai.init(); // just to trick TypeScript into recognizing the service decorators return this; } static hook(modules) { // just for decorator processing and registring } static connect() { } static getRegistry() { return this.rpcRegistry; } static async generateRPCMethodsList() { const methods = []; // Get the registry through the public method const registry = _a.getRegistry(); for (const [fullMethodName, registryEntryRaw] of Object.entries(registry)) { const registryEntry = registryEntryRaw; const [serviceName, methodName] = fullMethodName.split('.'); const methodInfo = { name: fullMethodName, description: registryEntry.doc, permission: registryEntry.auth }; // Convert input schema to TypeScript type if it exists if (registryEntry.inputSchema) { try { methodInfo.input_schema = (0, zodToTs_1.zodToTs)(registryEntry.inputSchema); } catch (error) { // console.warn(`Failed to convert input schema for ${fullMethodName}:`, error); methodInfo.input_schema = null; } } // Convert output schema to TypeScript type if it exists if (registryEntry.outputSchema) { try { methodInfo.output_schema = (0, zodToTs_1.zodToTs)(registryEntry.outputSchema); } catch (error) { // console.warn(`Failed to convert output schema for ${fullMethodName}:`, error); methodInfo.output_schema = null; } } methods.push(methodInfo); } const origin_functions = await this.getOriginFunctions(); // console.log(origin_functions, 'Origin functions fetched and added to method list.'); return [...methods, ...origin_functions]; } static async getOriginFunctions() { // fetch origins from server if not already fetched try { if (Object.keys(ttcAI_1.ttcAI.origins).length === 0) { try { const response = await _a.server.ttcCore.fetchOrigins(_a.app_id); // console.log(response, 'Fetched origins from server:'); if (response.status === 'success') { ttcAI_1.ttcAI.origins = response.data.reduce((acc, origin) => { acc[origin.name] = origin; return acc; }, {}); // console.log(ttcAI.origins, 'Stored origins:'); } } catch (error) { console.log('Error fetching origins:', error); return []; } } let origin_functions = []; for (const origin of Object.values(ttcAI_1.ttcAI.origins)) { const origin_result = await this.fetchOriginFunction(origin.url); const edited_origin_result = origin_result.map((func) => ({ ...func, name: `${origin.name}.${func.name}` // Prefix function name with origin name })); origin_functions = [...origin_functions, ...edited_origin_result]; } // console.log(origin_functions, 'Fetched origin functions:'); return origin_functions; } catch (error) { // console.log('Unable to fetch origins from server, confirm the server is running and the URL is correct.'); return []; } } static async fetchOriginFunction(url) { try { const query = `?sapikey=attaboy_bullshit`; const response = await fetch(`${url}/api-docs${query}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${_a.publickKey}` }, }); const result = await response.json(); // convert to array const final_array = Object.entries(result.services).flatMap(([serviceName, funcs]) => funcs.map(func => ({ ...func, name: `${serviceName}.${func.name}` }))); return final_array; } catch (error) { // console.log('Unable to fetch functions from server, confirm the server is running and the URL is correct and ask ttc to fetch functions again.'); return []; } } static async invoke_origin(parts, call, _scid) { try { const [originName, className, methodName] = parts; const origin = ttcAI_1.ttcAI.origins[originName]; if (!origin.url) throw new Error(`Unknown origin: ${originName}`); const response = await fetch(`${origin.url}/rpc`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${origin.apikey}` }, body: JSON.stringify({ method: `${className}.${methodName}`, params: Object.values(call.arguments) }) }); if (!response.ok) { // console.log(url, call, response); return response.statusText; // throw new Error(`API error: ${response.status} ${response.statusText}`); } return await response.json(); } catch (error) { console.error("Error calling API:", error); throw error; } } static async approveFunction(id, approval) { const cb = this.permissions[id]; // console.log(cb, 'Approved'); if (cb) { cb(approval); delete this.permissions[id]; } } static async askPermission(id, func, cb) { const key = `${id}-${func.function}`; if (!this.permissions[key]) { this.permissions[key] = cb; } await this.emit('permission', { id: key, payload: func }); } static async subscribe(event, cb) { this.event.on(event, cb); } static async emit(event, data) { this.event.emit(event, data); } } exports.ttc = ttc; _a = ttc; ttc.rpcRegistry = {}; ttc.permissions = {}; ttc.describe = (config) => (target, propertyKey, descriptor) => { const className = target.constructor.name; const method = descriptor.value; let params = {}; let generatedInputSchema; const paramRegex = /\(([^)]*)\)/; const paramsMatch = paramRegex.exec(method.toString()); const paramNames = paramsMatch ? paramsMatch[1] .split(",") .map((p) => p.trim()) .filter(Boolean) : []; // Get parameter types from reflection const types = Reflect.getMetadata("design:paramtypes", target, propertyKey) || []; const paramTypes = types.map((type) => type?.name || "unknown"); if (config?.inputSchema) { // Use provided schema const schemaAsJson = zodToJsonSchema(config.inputSchema); const schemaProperties = _a.getZodTypeName(schemaAsJson.properties || {}); // If the method has one argument, assume the schema defines its type. if (paramNames.length === 1) { const singleParam = paramNames[0]; // If the schemaProperties describes the inner fields of the single parameter // (e.g. { name: 'string', email: 'string' }) keep it as the param value. // If schemaProperties describes a single primitive (e.g. { userId: 'string' }), // unwrap it to avoid double-nesting: { userId: { userId: 'string' } } -> { userId: 'string' }. if (Object.keys(schemaProperties).length === 1 && schemaProperties.hasOwnProperty(singleParam)) { params[singleParam] = schemaProperties[singleParam]; } else { params[singleParam] = schemaProperties; } } else { // Otherwise, map schema properties to parameter names. params = schemaProperties; } generatedInputSchema = config.inputSchema; } else { // Generate schema from parameter types if (paramNames.length > 0) { generatedInputSchema = _a.createZodSchemaFromParams(paramNames, types); // Extract params from generated schema for compatibility const schemaAsJson = zodToJsonSchema(generatedInputSchema); const schemaProperties = _a.getZodTypeName(schemaAsJson.properties || {}); if (paramNames.length === 1) { const singleParam = paramNames[0]; if (Object.keys(schemaProperties).length === 1 && schemaProperties.hasOwnProperty(singleParam)) { params[singleParam] = schemaProperties[singleParam]; } else { params[singleParam] = schemaProperties; } } else { params = schemaProperties; } } else { // No parameters, create empty schema generatedInputSchema = zod_1.z.object({}); } } if (!typedi_1.default.has(className)) { typedi_1.default.set(className, new target.constructor()); } const key = `${className}.${propertyKey}`; _a.rpcRegistry[key] = { params, param_index: paramNames.length, inputSchema: generatedInputSchema, outputSchema: config?.outputSchema, doc: config?.doc, validate: config?.validate }; // console.log(key, params, 'Generated schema:', generatedInputSchema); }; ttc.handleSocket = async (socket) => { _a.socket = socket; socket.on("connect_error", (err) => { console.error("Socket connection error:", err.message || err); }); socket.on("function_call", async (data) => { const { conversation_id, functions } = data; await _a.ai.processFunctionCall(conversation_id, functions); }); }; //# sourceMappingURL=core.js.map