@draftor/tools
Version:
A simple TypeScript/Javascript functions to openai tool call format
159 lines (133 loc) • 4.46 kB
text/typescript
import * as doctrine from 'doctrine';
import { IParam, ITools, ITool, IFunctionDescription, IToolCall, IFunctionCall } from './types';
export class Tools implements ITools {
private queue: Array<{ toolCall: IToolCall | string; args?: object }> = [];
private isProcessing = false;
public funcs: Function[] | undefined;
constructor(funcs: Function[]) {
this.funcs = funcs;
}
private extractFirstJsDocComment(func: Function) {
const jsDocRegex = /\/\*\*([\s\S]*?)\*\//;
const match = jsDocRegex.exec(func.toString());
return match ? match[0] : null;
}
private transformDoctrineToOpenAITool(
doctrineOutput: any,
func: Function
): ITool {
const { description, tags } = doctrineOutput;
const params: IParam = {
type: 'object',
properties: {},
};
let returnType = 'void';
let returnDescription = '';
tags.forEach((tag: any) => {
if (tag.title === 'param') {
const param = tag?.name as string;
const type = tag?.type?.name || 'any';
const desc = tag?.description || '';
console.log(typeof desc);
params.properties[param] = {
type,
description: desc,
};
}
if (tag.title === 'returns') {
returnType = tag.type?.name || 'any';
returnDescription = tag.description || '';
}
});
const funcDesc: IFunctionDescription = {
name: func.name,
description: description?.trim() || 'Description unavailable.',
parameters: params,
};
const tool: ITool = {
type: 'function',
function: funcDesc,
};
return tool;
//
}
toOpenAI(type?: string): ITool[] | string {
if (this.funcs && this.funcs?.length !== 0) {
// reducer
const processedFunctions = this.funcs.reduce((acc, func) => {
const comment = this.extractFirstJsDocComment(func);
const doc = doctrine.parse(comment || '', { unwrap: true });
const transformed = this.transformDoctrineToOpenAITool(
doc,
func
) as unknown as ITool;
acc.push(transformed);
return acc;
}, [] as ITool[]);
return type == 'string' ? JSON.stringify(processedFunctions) : processedFunctions;
}
return '';
}
/**
* Processes the queue of function calls
* @returns Array of results from executed functions
*/
private async processQueue(): Promise<any[]> {
this.isProcessing = true;
const results: any[] = [];
while (this.queue.length > 0) {
const { toolCall, args } = this.queue.shift()!;
try {
const result = this.exec(toolCall, args);
results.push(result);
} catch (error) {
console.error('Error executing function:', error);
results.push(null);
}
}
this.isProcessing = false;
return results;
}
/**
* Adds function calls to queue and processes them
* @param toolCalls Array of tool calls or function names
* @param args Optional arguments for each call
*/
async executeAll(toolCalls: Array<IToolCall | string>, args?: object[]): Promise<any[]> {
// Add calls to queue
toolCalls.forEach((call, index) => {
this.queue.push({
toolCall: call,
args: args?.[index]
});
});
// Process queue if not already processing
if (!this.isProcessing) {
return this.processQueue();
}
return [];
}
exec(toolCall: IToolCall | string, args?: object): any {
if (typeof toolCall !== 'string') {
const { function: func } = toolCall;
const { name, arguments: args } = func;
try {
const callArgs = JSON.parse(args);
const callFunc = this.funcs?.filter((e) => e.name == name)[0] as unknown as Function;
const x = Object.keys(callArgs).map(e => callArgs[e]);
return callFunc(...x);
} catch (e) {
console.log(e)
}
} else {
try {
const callFunc = this.funcs?.filter((e) => e.name == toolCall)[0] as unknown as Function;
const y = args ?? {};
const x = Object.keys(y).map((key: string) => (y as Record<string, any>)[key]);
return callFunc(...x);
} catch (e) {
console.log(e)
}
}
}
}