ttc-ai-client
Version:
TypeScript client sdk for TTC AI services with decorators and schema validation.
443 lines • 17.7 kB
JavaScript
;
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