UNPKG

@firefliesai/schema-forge

Version:

Transform TypeScript classes into JSON Schema definitions with automatic support for OpenAI, Anthropic, and Google Gemini function calling (tool) formats

244 lines (243 loc) 14.9 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); require("reflect-metadata"); const lodash_1 = require("lodash"); const complex_class_tool_dto_1 = require("./fixture/complex-class.tool.dto"); const simple_class_tool_dto_1 = require("./fixture/simple-class.tool.dto"); const schema_forge_1 = require("./schema-forge"); describe('schema-forge test', () => { it('1 simple classes: classToJsonSchema, inheritance, classToJsonSchema with temp updated property, updateSchemaProperty (permanently)', async () => { const user2JsonSchemaTempChangeID2 = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User2, { propertyOverrides: { id2: { description: 'temp updated id2 description' }, }, }); expect(user2JsonSchemaTempChangeID2).toMatchSnapshot('1-1 inheritance class: classToJsonSchema with temp updated property'); const user2JsonSchema = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User2); expect(user2JsonSchema).toMatchSnapshot('1-2 inheritance class: classToJsonSchema and should not affected by temp updated property'); (0, schema_forge_1.updateSchemaProperty)(simple_class_tool_dto_1.User2, 'id2', { description: 'permanently updated id2 description', }); const user2JsonSchemaPersistChangeID2 = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User2); expect(user2JsonSchemaPersistChangeID2).toMatchSnapshot('1-3 inheritance class: updateSchemaProperty desc (permanently) and classToJsonSchema'); const userJsonSchema = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User); expect(userJsonSchema).toMatchSnapshot('1-4 parent class: classToJsonSchema (should not be affected by child class update)'); }); it('2 complex: array and enum class: classToJsonSchema, classToOpenAITool, updateSchemaProperty (permanently) w/ enum, ', async () => { const gameCharSchema = (0, schema_forge_1.classToJsonSchema)(complex_class_tool_dto_1.GameCharacter); expect(gameCharSchema).toMatchSnapshot('2-1 complex classToJsonSchema'); const gameCharTool = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.GameCharacter); expect(gameCharTool.type).toBe('function'); expect(gameCharTool.function.name).toBe(complex_class_tool_dto_1.GameCharacterToolName); expect(gameCharTool.function.description).toBe(complex_class_tool_dto_1.GameCharacterToolDesc); expect((0, lodash_1.isEqual)(gameCharTool.function.parameters, gameCharSchema)).toBe(true); /** updateSchemaProperty: enum, enum array */ (0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacter, 'roles', { enum: ['hero'], }); (0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacter, 'status', { enum: ['unknown'], }); (0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacter, 'level', { description: 'Updated level description', }); const gameCharUpdatedSchema = (0, schema_forge_1.classToJsonSchema)(complex_class_tool_dto_1.GameCharacter); gameCharSchema.properties.status.enum = ['unknown']; gameCharSchema.properties.roles.items.enum = ['hero']; gameCharSchema.properties.level.description = 'Updated level description'; expect((0, lodash_1.isEqual)(gameCharUpdatedSchema, gameCharSchema)).toBe(true); }); it('3 complex nested_object class: classToOpenAITool, updateSchemaProperty (permanently) w/ enum, ', async () => { const gameCharV2Tool = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.GameCharacterV2); expect(gameCharV2Tool).toMatchSnapshot('3-1 complex nested_object: classToOpenAITool'); (0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacterV2, 'banks.bankName', { description: 'New bankname description', }); const gameCharV2Tool2 = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.GameCharacterV2, { propertyOverrides: { location: { description: 'New location description', }, 'location.country': { description: 'New country description', }, }, }); gameCharV2Tool.function.parameters.properties.location.description = 'New location description'; gameCharV2Tool.function.parameters.properties.banks.items.properties.bankName.description = 'New bankname description'; gameCharV2Tool.function.parameters.properties.location.properties.country.description = 'New country description'; expect((0, lodash_1.isEqual)(gameCharV2Tool2, gameCharV2Tool)).toBe(true); }); it('4 complex nested nested three layer class: classToOpenAITool,updateSchemaProperty (permanently) w/ enum ', async () => { (0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.FirstLevelDto, 'secondLevelObj.thirdLevelObjs.name', { enum: ['E', 'F', 'G', 'H'], }); const firstLevelDto = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.FirstLevelDto); expect(firstLevelDto).toMatchSnapshot('4-1 complex nested nested three layer class: updateSchemaProperty and classToOpenAITool'); }); it('5 addSchemaProperty case', async () => { class TicketLLMAnswer { } (0, schema_forge_1.addSchemaProperty)(TicketLLMAnswer, 'ticketTitle1', { type: 'string', description: 'answer', }); (0, schema_forge_1.addSchemaProperty)(TicketLLMAnswer, 'ticketTitle2', { enum: ['optionName1', 'optionName2'], isOptional: true, }); const ticketLLMAnswerSchema = (0, schema_forge_1.classToJsonSchema)(TicketLLMAnswer); expect(ticketLLMAnswerSchema).toMatchSnapshot('5-1 addSchemaProperty case'); }); it('6 class with ToolProp() case', async () => { class SimpleAnswer { } __decorate([ (0, schema_forge_1.ToolProp)(), __metadata("design:type", String) ], SimpleAnswer.prototype, "answer", void 0); const schema = (0, schema_forge_1.classToJsonSchema)(SimpleAnswer); expect(schema).toMatchSnapshot('6-1 class with ToolProp()'); }); it('7 structured output enhancement', async () => { // Test enhanced JSON Schema const userSchemaEnhanced = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User, { forStructuredOutput: true }); expect(userSchemaEnhanced).toMatchSnapshot('7-1 enhanced JSON Schema'); // Test OpenAI function calling format const userToolEnhanced = (0, schema_forge_1.classToOpenAITool)(simple_class_tool_dto_1.User, { forStructuredOutput: true }); expect(userToolEnhanced).toMatchSnapshot('7-2 enhanced OpenAI function calling format'); // Test OpenAI response_format const userJsonSchemaFormat = (0, schema_forge_1.classToOpenAIResponseFormatJsonSchema)(simple_class_tool_dto_1.User, { forStructuredOutput: true, }); expect(userJsonSchemaFormat).toMatchSnapshot('7-3 OpenAI JSON Schema format for response_format'); }); it('8 different LLM formats', async () => { // Test Gemini tool format const geminiTool = (0, schema_forge_1.classToGeminiTool)(simple_class_tool_dto_1.User); expect(geminiTool).toMatchSnapshot('8-1 Gemini tool format'); // Test Anthropic tool format const anthropicTool = (0, schema_forge_1.classToAnthropicTool)(simple_class_tool_dto_1.User); expect(anthropicTool).toMatchSnapshot('8-2 Anthropic tool format'); // Test Gemini response schema const geminiResponseSchema = (0, schema_forge_1.classToGeminiResponseSchema)(simple_class_tool_dto_1.User); expect(geminiResponseSchema).toMatchSnapshot('8-3 Gemini response schema'); }); it('9 optional properties handling for different LLM providers', async () => { // Define nested DTOs first class AddressDto { } __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Street address' }), __metadata("design:type", String) ], AddressDto.prototype, "street", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'City name', isOptional: true }), __metadata("design:type", String) ], AddressDto.prototype, "city", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Postal code' }), __metadata("design:type", String) ], AddressDto.prototype, "postalCode", void 0); class ContactDto { } __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Contact name' }), __metadata("design:type", String) ], ContactDto.prototype, "name", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Contact email', isOptional: true }), __metadata("design:type", String) ], ContactDto.prototype, "email", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Phone number' }), __metadata("design:type", String) ], ContactDto.prototype, "phone", void 0); // Create a test class with optional properties including nested objects class TestWithOptionals { } __decorate([ (0, schema_forge_1.ToolProp)({ description: 'User ID' }), __metadata("design:type", String) ], TestWithOptionals.prototype, "id", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Username', isOptional: true }), __metadata("design:type", String) ], TestWithOptionals.prototype, "username", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Age of the user', isOptional: true }), __metadata("design:type", Number) ], TestWithOptionals.prototype, "age", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Tags for the user', items: { type: 'string' }, isOptional: true, }), __metadata("design:type", Array) ], TestWithOptionals.prototype, "tags", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'Primary address information', isOptional: true }), __metadata("design:type", AddressDto) ], TestWithOptionals.prototype, "address", void 0); __decorate([ (0, schema_forge_1.ToolProp)({ description: 'List of contact methods', items: { type: ContactDto }, isOptional: true, }), __metadata("design:type", Array) ], TestWithOptionals.prototype, "contacts", void 0); // Test OpenAI format handles optional properties with ["type", "null"] const openaiTool = (0, schema_forge_1.classToOpenAITool)(TestWithOptionals, { forStructuredOutput: true }); expect(openaiTool.function.parameters.properties.username.type).toEqual(['string', 'null']); expect(openaiTool.function.parameters.properties.age.type).toEqual(['number', 'null']); expect(openaiTool.function.parameters.properties.tags.type).toEqual(['array', 'null']); expect(openaiTool.function.parameters.properties.address.type).toEqual(['object', 'null']); expect(openaiTool.function.parameters.properties.contacts.type).toEqual(['array', 'null']); expect(openaiTool).toMatchSnapshot('9-1 OpenAI optional properties with nested objects'); // Test Gemini format properly marks properties as optional (not in required array) const geminiTool = (0, schema_forge_1.classToGeminiTool)(TestWithOptionals); // Check that optional properties are not in the required array expect(geminiTool.parameters.required).not.toContain('username'); expect(geminiTool.parameters.required).not.toContain('age'); expect(geminiTool.parameters.required).not.toContain('tags'); expect(geminiTool.parameters.required).not.toContain('address'); expect(geminiTool.parameters.required).not.toContain('contacts'); // Required properties should be in the required array expect(geminiTool.parameters.required).toContain('id'); expect(geminiTool).toMatchSnapshot('9-2 Gemini optional properties handling'); // Test Gemini response schema also properly marks properties as optional const geminiResponseSchema = (0, schema_forge_1.classToGeminiResponseSchema)(TestWithOptionals); // Check that optional properties are not in the required array expect(geminiResponseSchema.required).not.toContain('username'); expect(geminiResponseSchema.required).not.toContain('age'); expect(geminiResponseSchema.required).not.toContain('tags'); expect(geminiResponseSchema.required).not.toContain('address'); expect(geminiResponseSchema.required).not.toContain('contacts'); // Required properties should be in the required array expect(geminiResponseSchema.required).toContain('id'); expect(geminiResponseSchema).toMatchSnapshot('9-3 Gemini response schema optional properties handling'); // Verify that optionals are properly handled in nested objects expect(openaiTool.function.parameters.properties.address.properties.city.type).toEqual([ 'string', 'null', ]); // For Gemini, verify city is not in address's required array const addressProps = geminiTool.parameters.properties.address; expect(addressProps.properties.city).toBeDefined(); const addressRequired = addressProps.required || []; expect(addressRequired).not.toContain('city'); }); });