UNPKG

@agentica/core

Version:

Agentic AI Library specialized in LLM Function Calling

370 lines (310 loc) 11.9 kB
import type { IChatGptSchema, IHttpLlmFunction, ILlmFunction, IMcpLlmFunction, IValidation } from "@samchon/openapi"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory"; import { createServer } from "@wrtnlabs/calculator-mcp"; import type { IAgenticaConfig } from "../../structures/IAgenticaConfig"; import type { IAgenticaController } from "../../structures/IAgenticaController"; import { assertMcpController } from "../../functional/assertMcpController"; import { compose, divide, getOperations, toClassOperations, toHttpOperations, toMcpOperations } from "./AgenticaOperationComposer"; const client = new Client({ name: "calculator", version: "1.0.0", }); // test helper functions function createMockHttpFunction(name: string, method: "get" | "post" | "patch" | "put" | "delete", path: string): IHttpLlmFunction<any> { return { name, method, path, validate: () => ({ success: true, data: {} } as IValidation<unknown>), operation: () => ({}), route: () => ({ method, path, emendedPath: path, accessor: [name], body: null, query: null, parameters: [], headers: null, success: null, exceptions: {}, comment: () => "OK", operation: () => ({}), }), parameters: {}, output: {}, }; } function createMockHttpController(name: string, functions: IHttpLlmFunction<any>[]): IAgenticaController.IHttp<any> { return { name, protocol: "http", connection: { host: "https://example.com" }, application: { model: "chatgpt", options: {}, functions, errors: [], }, }; } function createMockClassController(name: string, functions: ILlmFunction<any>[]): IAgenticaController.IClass<any> { return { name, protocol: "class", application: { model: "chatgpt", options: {}, functions, }, execute: {}, }; } async function createMockMcpController(name: string, functions: IMcpLlmFunction<"chatgpt">[]): Promise<IAgenticaController.IMcp<"chatgpt">> { const controller = await assertMcpController({ model: "chatgpt", name, client, }); return { ...controller, application: { ...controller.application, functions, }, }; } describe("a AgenticaOperationComposer", () => { beforeAll(async () => { const server = await createServer({ name: "calculator", version: "1.0.0", }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); }); describe("compose", () => { it("should compose operations from controllers", async () => { // Mock controllers const mockHttpController = createMockHttpController("httpController", [ createMockHttpFunction("function1", "get", "/api/function1"), createMockHttpFunction("function2", "post", "/api/function2"), ]); const mockClassController = createMockClassController("classController", [ { name: "function3", validate: () => ({ success: true, data: {} } as IValidation<unknown>), parameters: {}, output: {}, }, ]); const mockMcpController = await createMockMcpController("mcpController", [ { name: "function4", parameters: { type: "object", properties: {}, additionalProperties: false, required: [], $defs: {}, } satisfies IChatGptSchema.IParameters, validate: (data: unknown) => ({ success: true, data, }), }, ]); const controllers = [mockHttpController, mockClassController, mockMcpController]; const result = compose({ controllers }); expect(result.array).toHaveLength(4); expect(result.flat).toBeInstanceOf(Map); expect(result.group).toBeInstanceOf(Map); expect(result.divided).toBeUndefined(); }); it("should divide operations when capacity is provided", () => { // Mock controllers const mockController = createMockHttpController("httpController", [ createMockHttpFunction("function1", "get", "/api/function1"), createMockHttpFunction("function2", "post", "/api/function2"), createMockHttpFunction("function3", "put", "/api/function3"), createMockHttpFunction("function4", "delete", "/api/function4"), createMockHttpFunction("function5", "patch", "/api/function5"), ]); const config: IAgenticaConfig<any> = { capacity: 2, }; const result = compose({ controllers: [mockController], config }); expect(result.array).toHaveLength(5); expect(result.divided).toBeDefined(); expect(result.divided).toHaveLength(3); // 5 items with capacity 2 should be divided into 3 groups }); }); describe("getOperations", () => { it("should get operations from http controllers", () => { const mockController = createMockHttpController("httpController", [ createMockHttpFunction("function1", "get", "/api/function1"), createMockHttpFunction("function2", "post", "/api/function2"), ]); const result = getOperations({ controllers: [mockController], naming: (func, idx) => `_${idx}_${func}` }); expect(result).toHaveLength(2); expect(result[0]?.protocol).toBe("http"); expect(result[0]?.name).toBe("_0_function1"); expect(result[1]?.name).toBe("_0_function2"); }); it("should get operations from class controllers", () => { const mockController = createMockClassController("classController", [ { name: "function1", validate: () => ({ success: true, data: {} } as IValidation<unknown>), parameters: {}, output: {}, }, ]); const result = getOperations({ controllers: [mockController], naming: (func, idx) => `_${idx}_${func}` }); expect(result).toHaveLength(1); expect(result[0]?.protocol).toBe("class"); expect(result[0]?.name).toBe("_0_function1"); }); it("should get operations from mcp controllers", async () => { const mockController = await createMockMcpController("mcpController", [ { name: "function1", parameters: { type: "object", properties: {}, additionalProperties: false, required: [], $defs: {}, }, validate: (data: unknown) => ({ success: true, data, }), }, ]); const result = getOperations({ controllers: [mockController], naming: (func, idx) => `_${idx}_${func}` }); expect(result).toHaveLength(1); expect(result[0]?.protocol).toBe("mcp"); expect(result[0]?.name).toBe("_0_function1"); }); it("should throw error for unsupported protocol", () => { const mockController: IAgenticaController.IHttp<any> = { name: "unsupportedController", protocol: "unsupported" as unknown as "http", connection: { host: "https://example.com" }, application: { } as unknown as IAgenticaController.IHttp<any>["application"], }; expect(() => getOperations({ controllers: [mockController], naming: (func, idx) => `_${idx}_${func}` })).toThrow("Unsupported protocol: unsupported"); }); }); describe("toHttpOperations", () => { it("should convert http controller to operations", () => { const mockController = createMockHttpController("httpController", [ createMockHttpFunction("function1", "get", "/api/function1"), createMockHttpFunction("function2", "post", "/api/function2"), ]); const result = toHttpOperations({ controller: mockController, index: 0, naming: (func, idx) => `_${idx}_${func}` }); expect(result).toHaveLength(2); expect(result[0]?.protocol).toBe("http"); expect(result[0]?.name).toBe("_0_function1"); expect(result[1]?.name).toBe("_0_function2"); }); }); describe("toClassOperations", () => { it("should convert class controller to operations", () => { const mockController = createMockClassController("classController", [ { name: "function1", validate: () => ({ success: true, data: {} } as IValidation<unknown>), parameters: {}, output: {}, }, ]); const result = toClassOperations({ controller: mockController, index: 0, naming: (func, idx) => `_${idx}_${func}` }); expect(result).toHaveLength(1); expect(result[0]?.protocol).toBe("class"); expect(result[0]?.name).toBe("_0_function1"); }); }); describe("toMcpOperations", () => { it("should convert mcp controller to operations", async () => { const mockController = await createMockMcpController("mcpController", [ { name: "function1", parameters: { type: "object", properties: {}, additionalProperties: false, required: [], $defs: {}, }, validate: (data: unknown) => ({ success: true, data, }), }, ]); const result = toMcpOperations({ controller: mockController, index: 0, naming: (func, idx) => `_${idx}_${func}` }); expect(result).toHaveLength(1); expect(result[0]?.protocol).toBe("mcp"); expect(result[0]?.name).toBe("_0_function1"); }); }); describe("divide with invalid capacity", () => { it("should throw error when capacity is 0", () => { const array = [1, 2, 3, 4, 5]; const capacity = 0; expect(() => divide({ array, capacity })).toThrow("Capacity must be a positive integer"); }); it("should throw error when capacity is negative", () => { const array = [1, 2, 3, 4, 5]; const capacity = -3; expect(() => divide({ array, capacity })).toThrow("Capacity must be a positive integer"); }); it("should throw error when capacity is decimal", () => { const array = [1, 2, 3, 4, 5]; const capacity = 2.5; const result = divide({ array, capacity }); expect(result).toEqual([[1, 2, 3], [4, 5]]); }); it("should throw error when capacity is Infinity", () => { const array = [1, 2, 3, 4, 5]; const capacity = Infinity; expect(() => divide({ array, capacity })).toThrow("Capacity must be a positive integer"); }); it("should throw error when capacity is NaN", () => { const array = [1, 2, 3, 4, 5]; const capacity = Number.NaN; expect(() => divide({ array, capacity })).toThrow("Capacity must be a positive integer"); }); }); describe("divide", () => { it("should divide array into chunks based on capacity", () => { const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const capacity = 3; const result = divide({ array, capacity }); expect(result).toHaveLength(4); // 10 items with capacity 3 should be divided into 4 groups expect(result[0]).toEqual([1, 2, 3]); expect(result[1]).toEqual([4, 5, 6]); expect(result[2]).toEqual([7, 8, 9]); expect(result[3]).toEqual([10]); }); it("should handle empty array", () => { const array: number[] = []; const capacity = 3; const result = divide({ array, capacity }); expect(result).toHaveLength(0); }); it("should handle array smaller than capacity", () => { const array = [1, 2]; const capacity = 3; const result = divide({ array, capacity }); expect(result).toHaveLength(1); expect(result[0]).toEqual([1, 2]); }); }); });