diff --git a/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/ESValueSummaryGenerator.js b/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/ESValueSummaryGenerator.js new file mode 100644 index 0000000..8ff7799 --- /dev/null +++ b/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/ESValueSummaryGenerator.js @@ -0,0 +1,296 @@ + +const FormattedJSONAIGenerator = require("../../FormattedJSONAIGenerator"); +const { ESValueSummarizeWithFilter } = require("../helpers") +const ESAliasTransManager = require("../../es-ddl-generators/modules/ESAliasTransManager") +const { TokenCounter } = require("../../utils") + +/** + * @description EventStorming 모델의 요약 정보를 생성하고 관리하는 클래스입니다. + * 주어진 컨텍스트에 따라 EventStorming 요소들을 분석하고 토큰 제한에 맞춰 요약된 정보를 생성합니다. + * + * @class + * @extends {FormattedJSONAIGenerator} + * + * @property {string[]} checkInputParamsKeys - 필수 입력 파라미터 키 목록 + * @property {string[]} progressCheckStrings - 진행 상태 확인을 위한 문자열 목록 + * @property {ESAliasTransManager} esAliasTransManager - ES 별칭 변환 관리자 + * + * @constructor + * @param {Object} client - 클라이언트 설정 객체 + * @param {Object} client.input - 입력 파라미터 + * @param {string} client.input.context - 분석 컨텍스트 + * @param {Object} client.input.esValue - EventStorming 모델 데이터 + * @param {string[]} client.input.keysToFilter - 필터링할 키 목록 + * @param {number} client.input.maxTokens - 최대 토큰 수 제한 + * @param {string} client.input.tokenCalcModel - 토큰 계산 모델 + * + * @throws {Error} id 속성이 keysToFilter에 포함된 경우 + * + * @see FormattedJSONAIGenerator + * @see ESAliasTransManager + * @see ESValueSummarizeWithFilter + * + * @example + * const generator = new ESValueSummaryGenerator({ + * input: { + * context: "도서 관련 커맨드 생성 작업을 수행해야 함", + * esValue: libraryEsValue, + * keysToFilter: [], + * maxTokens: 800, + * tokenCalcModel: "gpt-4o" + * }, + * onModelCreated: (returnObj) => { + * // 모델 생성 완료 시 처리 + * }, + * onGenerationSucceeded: (returnObj) => { + * // 생성 성공 시 처리 + * console.log("요약 토큰 수:", TokenCounter.getTokenCount( + * JSON.stringify(returnObj.modelValue.summary), + * "gpt-4o" + * )); + * } + * }); + * + * generator.generate(); + * + * @note + * - id 속성은 정렬에 사용되므로 필터링할 수 없습니다 + * - 토큰 제한에 맞춰 자동으로 요약 정보가 조정됩니다 + * - 요약된 정보는 JSON 형식으로 제공됩니다 + */ +class ESValueSummaryGenerator extends FormattedJSONAIGenerator{ + constructor(client){ + super(client); + + this.checkInputParamsKeys = ["context", "esValue", "keysToFilter", "maxTokens", "tokenCalcModel"] + this.progressCheckStrings = ["overviewThoughts", "result"] + } + + + onGenerateBefore(inputParams){ + if(inputParams.keysToFilter.includes("id")) + throw new Error("id 속성은 정렬에 사용되기 때문에 필터링에서 제외되어야 합니다.") + + inputParams.esValue = JSON.parse(JSON.stringify(inputParams.esValue)) + this.esAliasTransManager = new ESAliasTransManager(inputParams.esValue) + + inputParams.summarizedESValue = ESValueSummarizeWithFilter.getSummarizedESValue( + inputParams.esValue, inputParams.keysToFilter, this.esAliasTransManager + ) + } + + + __buildAgentRolePrompt(){ + return `You are an expert context analyzer and DDD specialist focusing on event storming elements. Your expertise includes: +- Analyzing relationships between different event storming elements in a given context +- Understanding dependencies and hierarchies in domain-driven design +- Evaluating the relevance and importance of elements based on context +- Organizing and prioritizing elements based on their contextual relationships +- Identifying core domain concepts and their supporting elements +` + } + + __buildTaskGuidelinesPrompt(){ + return `You are given a list of IDs for each element used in event stemming, and you need to return them sorted in order of relevance to the context in which they were passed. + +Please follow these rules: +1. Do not write comments in the output JSON object. + +Here's what each prefix in the element IDs means: +- bc-: Bounded Context +- act-: Actor +- agg-: Aggregate +- cmd-: Command +- evt-: Event +- rm-: Read Model +- enum-: Enumeration +- vo-: Value Object + +For example, "bc-bookManagement" represents a Bounded Context named "bookManagement". +` + } + + __buildJsonResponseFormat() { + return ` +{ + "overviewThoughts": { + "summary": "Analysis of element relationships and their contextual relevance", + "details": { + "contextualAnalysis": "Evaluation of how elements relate to the given context and business domain", + "elementRelationships": "Analysis of dependencies and connections between different elements", + "sortingStrategy": "Explanation of the prioritization and sorting approach used" + }, + "additionalConsiderations": "Potential impact of element ordering on system understanding and documentation" + }, + + "result": { + "sortedElementIds": [ + "", + ... + ] + } +} +` + } + + __buildJsonExampleInputFormat() { + return { + "Context": "Adding cancel order command to Order aggregate", + "EventStorming Element Ids": [ + "bc-orderManagement", + "bc-productCatalog", + "bc-userManagement", + "act-customer", + "act-admin", + "agg-order", + "agg-product", + "agg-user", + "cmd-cancelOrder", + "cmd-createOrder", + "cmd-updateProduct", + "evt-orderCanceled", + "evt-orderCreated", + "evt-productUpdated", + "rm-orderHistory", + "rm-productInventory", + "enum-orderStatus", + "enum-productCategory", + "vo-address", + "vo-money" + ] + } + } + + __buildJsonExampleOutputFormat() { + return { + "overviewThoughts": { + "summary": "Analyzing elements related to order cancellation functionality", + "details": { + "contextualAnalysis": "Focus on order management bounded context and order cancellation process", + "elementRelationships": "Direct relationships between order aggregate, cancel command, and resulting event", + "sortingStrategy": "Prioritizing elements directly involved in order cancellation, followed by related order management elements, then peripheral elements" + }, + "additionalConsiderations": "Order cancellation may trigger inventory updates and affect order history views" + }, + + "result": { + "sortedElementIds": [ + "bc-orderManagement", + "agg-order", + "cmd-cancelOrder", + "evt-orderCanceled", + "act-customer", + "act-admin", + "enum-orderStatus", + "rm-orderHistory", + "cmd-createOrder", + "evt-orderCreated", + "vo-money", + "bc-productCatalog", + "agg-product", + "cmd-updateProduct", + "evt-productUpdated", + "rm-productInventory", + "enum-productCategory", + "bc-userManagement", + "agg-user", + "vo-address" + ] + } + } + } + + __buildJsonUserQueryInputFormat() { + return { + "Context": this.client.input.context, + "EventStorming Element Ids": this._getIdsFromSummarizedESValue(this.client.input.summarizedESValue), + } + } + + _getIdsFromSummarizedESValue(summarizedESValue) { + const ids = new Set(); + + const extractIds = (obj) => { + if (!obj || typeof obj !== 'object') return; + + if (obj.id) { + ids.add(obj.id); + } + + if (Array.isArray(obj)) { + obj.forEach(item => extractIds(item)); + } else { + Object.values(obj).forEach(value => extractIds(value)); + } + }; + + extractIds(summarizedESValue); + return Array.from(ids); + } + + + onCreateModelGenerating(returnObj){ + returnObj.directMessage = `Summarizing EventStorming Model... (${returnObj.modelRawValue.length} characters generated)` + } + + onCreateModelFinished(returnObj){ + const summary = this._getSummaryWithinTokenLimit( + this.client.input.summarizedESValue, + returnObj.modelValue.aiOutput.result.sortedElementIds, + this.client.input.maxTokens, + this.client.input.tokenCalcModel + ) + + returnObj.modelValue = { + ...returnObj.modelValue, + summary: summary, + } + returnObj.directMessage = `Summarizing EventStorming Model... (${returnObj.modelRawValue.length} characters generated)` + } + + _getSummaryWithinTokenLimit(summarizedESValue, sortedElementIds, maxTokens, tokenCalcModel) { + const elementIds = [...sortedElementIds]; + let priorityIndex = elementIds.length; + + let result = JSON.parse(JSON.stringify(summarizedESValue)); + + const filterByPriority = (obj) => { + if (Array.isArray(obj)) { + const filtered = obj + .filter(item => { + if (!item.id) return true; + const index = elementIds.indexOf(item.id); + return index !== -1 && index < priorityIndex; + }) + .sort((a, b) => { + if (!a.id || !b.id) return 0; + return elementIds.indexOf(a.id) - elementIds.indexOf(b.id); + }) + .map(item => filterByPriority(item)); + return filtered; + } else if (obj && typeof obj === 'object') { + const filtered = {}; + for (const [key, value] of Object.entries(obj)) { + filtered[key] = filterByPriority(value); + } + return filtered; + } + return obj; + }; + + while (priorityIndex > 0) { + const filtered = filterByPriority(result); + const jsonString = JSON.stringify(filtered); + + if (TokenCounter.isWithinTokenLimit(jsonString, tokenCalcModel, maxTokens)) { + return filtered; + } + + priorityIndex--; + } + + return filterByPriority(result); + } +} + +module.exports = ESValueSummaryGenerator; \ No newline at end of file diff --git a/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/index.js b/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/index.js index 8ff7799..e01b7d0 100644 --- a/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/index.js +++ b/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/index.js @@ -1,296 +1 @@ - -const FormattedJSONAIGenerator = require("../../FormattedJSONAIGenerator"); -const { ESValueSummarizeWithFilter } = require("../helpers") -const ESAliasTransManager = require("../../es-ddl-generators/modules/ESAliasTransManager") -const { TokenCounter } = require("../../utils") - -/** - * @description EventStorming 모델의 요약 정보를 생성하고 관리하는 클래스입니다. - * 주어진 컨텍스트에 따라 EventStorming 요소들을 분석하고 토큰 제한에 맞춰 요약된 정보를 생성합니다. - * - * @class - * @extends {FormattedJSONAIGenerator} - * - * @property {string[]} checkInputParamsKeys - 필수 입력 파라미터 키 목록 - * @property {string[]} progressCheckStrings - 진행 상태 확인을 위한 문자열 목록 - * @property {ESAliasTransManager} esAliasTransManager - ES 별칭 변환 관리자 - * - * @constructor - * @param {Object} client - 클라이언트 설정 객체 - * @param {Object} client.input - 입력 파라미터 - * @param {string} client.input.context - 분석 컨텍스트 - * @param {Object} client.input.esValue - EventStorming 모델 데이터 - * @param {string[]} client.input.keysToFilter - 필터링할 키 목록 - * @param {number} client.input.maxTokens - 최대 토큰 수 제한 - * @param {string} client.input.tokenCalcModel - 토큰 계산 모델 - * - * @throws {Error} id 속성이 keysToFilter에 포함된 경우 - * - * @see FormattedJSONAIGenerator - * @see ESAliasTransManager - * @see ESValueSummarizeWithFilter - * - * @example - * const generator = new ESValueSummaryGenerator({ - * input: { - * context: "도서 관련 커맨드 생성 작업을 수행해야 함", - * esValue: libraryEsValue, - * keysToFilter: [], - * maxTokens: 800, - * tokenCalcModel: "gpt-4o" - * }, - * onModelCreated: (returnObj) => { - * // 모델 생성 완료 시 처리 - * }, - * onGenerationSucceeded: (returnObj) => { - * // 생성 성공 시 처리 - * console.log("요약 토큰 수:", TokenCounter.getTokenCount( - * JSON.stringify(returnObj.modelValue.summary), - * "gpt-4o" - * )); - * } - * }); - * - * generator.generate(); - * - * @note - * - id 속성은 정렬에 사용되므로 필터링할 수 없습니다 - * - 토큰 제한에 맞춰 자동으로 요약 정보가 조정됩니다 - * - 요약된 정보는 JSON 형식으로 제공됩니다 - */ -class ESValueSummaryGenerator extends FormattedJSONAIGenerator{ - constructor(client){ - super(client); - - this.checkInputParamsKeys = ["context", "esValue", "keysToFilter", "maxTokens", "tokenCalcModel"] - this.progressCheckStrings = ["overviewThoughts", "result"] - } - - - onGenerateBefore(inputParams){ - if(inputParams.keysToFilter.includes("id")) - throw new Error("id 속성은 정렬에 사용되기 때문에 필터링에서 제외되어야 합니다.") - - inputParams.esValue = JSON.parse(JSON.stringify(inputParams.esValue)) - this.esAliasTransManager = new ESAliasTransManager(inputParams.esValue) - - inputParams.summarizedESValue = ESValueSummarizeWithFilter.getSummarizedESValue( - inputParams.esValue, inputParams.keysToFilter, this.esAliasTransManager - ) - } - - - __buildAgentRolePrompt(){ - return `You are an expert context analyzer and DDD specialist focusing on event storming elements. Your expertise includes: -- Analyzing relationships between different event storming elements in a given context -- Understanding dependencies and hierarchies in domain-driven design -- Evaluating the relevance and importance of elements based on context -- Organizing and prioritizing elements based on their contextual relationships -- Identifying core domain concepts and their supporting elements -` - } - - __buildTaskGuidelinesPrompt(){ - return `You are given a list of IDs for each element used in event stemming, and you need to return them sorted in order of relevance to the context in which they were passed. - -Please follow these rules: -1. Do not write comments in the output JSON object. - -Here's what each prefix in the element IDs means: -- bc-: Bounded Context -- act-: Actor -- agg-: Aggregate -- cmd-: Command -- evt-: Event -- rm-: Read Model -- enum-: Enumeration -- vo-: Value Object - -For example, "bc-bookManagement" represents a Bounded Context named "bookManagement". -` - } - - __buildJsonResponseFormat() { - return ` -{ - "overviewThoughts": { - "summary": "Analysis of element relationships and their contextual relevance", - "details": { - "contextualAnalysis": "Evaluation of how elements relate to the given context and business domain", - "elementRelationships": "Analysis of dependencies and connections between different elements", - "sortingStrategy": "Explanation of the prioritization and sorting approach used" - }, - "additionalConsiderations": "Potential impact of element ordering on system understanding and documentation" - }, - - "result": { - "sortedElementIds": [ - "", - ... - ] - } -} -` - } - - __buildJsonExampleInputFormat() { - return { - "Context": "Adding cancel order command to Order aggregate", - "EventStorming Element Ids": [ - "bc-orderManagement", - "bc-productCatalog", - "bc-userManagement", - "act-customer", - "act-admin", - "agg-order", - "agg-product", - "agg-user", - "cmd-cancelOrder", - "cmd-createOrder", - "cmd-updateProduct", - "evt-orderCanceled", - "evt-orderCreated", - "evt-productUpdated", - "rm-orderHistory", - "rm-productInventory", - "enum-orderStatus", - "enum-productCategory", - "vo-address", - "vo-money" - ] - } - } - - __buildJsonExampleOutputFormat() { - return { - "overviewThoughts": { - "summary": "Analyzing elements related to order cancellation functionality", - "details": { - "contextualAnalysis": "Focus on order management bounded context and order cancellation process", - "elementRelationships": "Direct relationships between order aggregate, cancel command, and resulting event", - "sortingStrategy": "Prioritizing elements directly involved in order cancellation, followed by related order management elements, then peripheral elements" - }, - "additionalConsiderations": "Order cancellation may trigger inventory updates and affect order history views" - }, - - "result": { - "sortedElementIds": [ - "bc-orderManagement", - "agg-order", - "cmd-cancelOrder", - "evt-orderCanceled", - "act-customer", - "act-admin", - "enum-orderStatus", - "rm-orderHistory", - "cmd-createOrder", - "evt-orderCreated", - "vo-money", - "bc-productCatalog", - "agg-product", - "cmd-updateProduct", - "evt-productUpdated", - "rm-productInventory", - "enum-productCategory", - "bc-userManagement", - "agg-user", - "vo-address" - ] - } - } - } - - __buildJsonUserQueryInputFormat() { - return { - "Context": this.client.input.context, - "EventStorming Element Ids": this._getIdsFromSummarizedESValue(this.client.input.summarizedESValue), - } - } - - _getIdsFromSummarizedESValue(summarizedESValue) { - const ids = new Set(); - - const extractIds = (obj) => { - if (!obj || typeof obj !== 'object') return; - - if (obj.id) { - ids.add(obj.id); - } - - if (Array.isArray(obj)) { - obj.forEach(item => extractIds(item)); - } else { - Object.values(obj).forEach(value => extractIds(value)); - } - }; - - extractIds(summarizedESValue); - return Array.from(ids); - } - - - onCreateModelGenerating(returnObj){ - returnObj.directMessage = `Summarizing EventStorming Model... (${returnObj.modelRawValue.length} characters generated)` - } - - onCreateModelFinished(returnObj){ - const summary = this._getSummaryWithinTokenLimit( - this.client.input.summarizedESValue, - returnObj.modelValue.aiOutput.result.sortedElementIds, - this.client.input.maxTokens, - this.client.input.tokenCalcModel - ) - - returnObj.modelValue = { - ...returnObj.modelValue, - summary: summary, - } - returnObj.directMessage = `Summarizing EventStorming Model... (${returnObj.modelRawValue.length} characters generated)` - } - - _getSummaryWithinTokenLimit(summarizedESValue, sortedElementIds, maxTokens, tokenCalcModel) { - const elementIds = [...sortedElementIds]; - let priorityIndex = elementIds.length; - - let result = JSON.parse(JSON.stringify(summarizedESValue)); - - const filterByPriority = (obj) => { - if (Array.isArray(obj)) { - const filtered = obj - .filter(item => { - if (!item.id) return true; - const index = elementIds.indexOf(item.id); - return index !== -1 && index < priorityIndex; - }) - .sort((a, b) => { - if (!a.id || !b.id) return 0; - return elementIds.indexOf(a.id) - elementIds.indexOf(b.id); - }) - .map(item => filterByPriority(item)); - return filtered; - } else if (obj && typeof obj === 'object') { - const filtered = {}; - for (const [key, value] of Object.entries(obj)) { - filtered[key] = filterByPriority(value); - } - return filtered; - } - return obj; - }; - - while (priorityIndex > 0) { - const filtered = filterByPriority(result); - const jsonString = JSON.stringify(filtered); - - if (TokenCounter.isWithinTokenLimit(jsonString, tokenCalcModel, maxTokens)) { - return filtered; - } - - priorityIndex--; - } - - return filterByPriority(result); - } -} - -module.exports = ESValueSummaryGenerator; \ No newline at end of file +export const ESValueSummaryGenerator = require("./ESValueSummaryGenerator"); \ No newline at end of file diff --git a/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/tests.js b/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/tests.js index fd7cc99..d3f2f31 100644 --- a/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/tests.js +++ b/src/components/designer/modeling/generators/es-generators/ESValueSummaryGenerator/tests.js @@ -1,5 +1,4 @@ - -const ESValueSummaryGenerator = require("./index"); +const ESValueSummaryGenerator = require("./ESValueSummaryGenerator") const { libraryEsValue } = require("./mocks"); const { ESValueSummarizeWithFilter } = require("../helpers") const ESAliasTransManager = require("../../es-ddl-generators/modules/ESAliasTransManager") diff --git a/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/ESValueSummarizeWithFilter.js b/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/ESValueSummarizeWithFilter.js new file mode 100644 index 0000000..bbe822f --- /dev/null +++ b/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/ESValueSummarizeWithFilter.js @@ -0,0 +1,654 @@ +const { TokenCounter } = require("../../../utils") + +class ESValueSummarizeWithFilter { + /** + * getSummarizedESValue()에서 반환될 값에 대한 상세한 입력 프롬프트 가이드 반환 + */ + static getGuidePrompt() { + return `You will receive a JSON object containing summarized information about the event storming model on which you will perform your task. Not all guided properties may be passed, and properties that are unnecessary for the task may be excluded. +The approximate structure is as follows. +{ + // Properties that have been deleted because they are not needed for the given task. + "deletedProperties": [""], + + "boundedContexts": [ + "id": "", + "name": "", + "actors": [ + { + "id": "", + "name": "" + } + ], + "aggregates": [ + { + "id": "", + "name": "", + "properties": [ + { + "name": "", + + // "" must belong to one of the following three categories: + // 1. Well-known Java class names. In this case, write only the class name without the full package path. (e.g., java.lang.String > String) + // 2. One of the following values: Address, Photo, User, Money, Email, Payment, Weather, File, Likes, Tags, Comment + // 3. If there's a name defined in Enumerations or ValueObjects or Entities, It can be used as the property type. + // If the type is String, do not specify the type. + ["type": ""], + + // Only one of the properties should have isKey set to true. + // If it needs a composite key, it will reference a ValueObject with those properties. + ["isKey": true] + } + ], + + "entities": [ + { + "id": "", + "name": "", + "properties": [ + { + "name": "", + ["type": ""], + ["isKey": true], + + // If this property references a property in another table, this value should be set to true. + ["isForeignProperty": true] + } + ] + } + ], + + "enumerations": [ + { + "id": "", + "name": "", + "items": [""] + } + ], + + "valueObjects": [ + { + "id": "", + "name": "", + "properties": [ + { + "name": "", + ["type": ""], + ["isKey": true], + ["isForeignProperty": true], + + // If the property is used as a foreign key to reference another Aggregate, write the name of that Aggregate. + ["referencedAggregateName": ""] + } + ] + } + ], + + "commands": [ + { + "id": "", + "name": "", + "api_verb": <"POST" | "PUT" | "PATCH" | "DELETE">, + + // Determines the API endpoint structure: + // true: Uses standard REST endpoints with HTTP verbs only (e.g., POST /book) + // false: Uses custom endpoints with command names for complex operations (e.g., POST /book/updateStatus) + "isRestRepository": , + "properties": [ + { + "name": "", + ["type": ""], + ["isKey": true] + } + ], + + // A list of cascading events that occur when a command is executed. + "outputEvents": [ + { + "id": "", + "name": "" + } + ] + } + ], + + "events": [ + { + "id": "", + "name": "", + "properties": [ + { + "name": "", + ["type": ""], + ["isKey": true] + } + ], + + // A list of cascading commands that occur when an event is executed. + "outputCommands": [ + { + "id": "", + "name": "", + "policyId": "", + "policyName": "" + } + ] + } + ], + + "readModels": [ + { + "id": "", + "name": "", + "properties": [ + { + "name": "", + ["type": ""], + ["isKey": true] + } + ], + "isMultipleResult": + } + ] + } + ] + ] +}` + } + + static async getSummarizedESValueWithMaxTokenSummarize(esValue, keysToFilter=[], esAliasTransManager=null, maxTokens=800, tokenCalcModel="gpt-4o"){ + const summarizedESValue = this.getSummarizedESValue( + esValue, keysToFilter, esAliasTransManager + ) + + const tokenCount = TokenCounter.getTokenCount(JSON.stringify(summarizedESValue), tokenCalcModel) + if(tokenCount < maxTokens) + return summarizedESValue + + throw new Error("토큰 수가 초과되었습니다.") + } + + /** + * 주어진 이벤트스토밍에서 위치 데이터등을 제외한 핵심 정보들만 추출해서 LLM에게 입력 데이터로 제공 + * keysToFilter: 일부 경우에는 properties와 같은 구체적인 속성들이 불필요할 수 있음. 이런 경우에 제외시킬 키값을 배열로 전달 + * ex) id: 모든 id 속성을 미포함, boundedContext.id: boundedContext의 id 속성을 미포함 + * esAliasTransManager: Id값을 별칭으로 바꿔서 UUID를 제거해서, 패턴 기반의 LLM의 성능을 향상 + */ + static getSummarizedESValue(esValue, keysToFilter=[], esAliasTransManager=null){ + const boundedContexts = Object.values(esValue.elements) + .filter(element => element && element._type === 'org.uengine.modeling.model.BoundedContext') + + let summarizedBoundedContexts = boundedContexts.map(boundedContext => + this.getSummarizedBoundedContextValue( + esValue, boundedContext, keysToFilter, esAliasTransManager + ) + ) + + return { + deletedProperties: keysToFilter, + boundedContexts: summarizedBoundedContexts + } + } + + + static getSummarizedBoundedContextValue(esValue, boundedContext, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + + this._restoreBoundedContextAggregatesProperties(esValue, boundedContext) + + return { + ...getConditionalValue( + ["id", "boundedContext.id"], + { id: this._getElementIdSafely(boundedContext, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "boundedContext.name"], + { name: boundedContext.name } + ), + ...getConditionalValue( + ["actors", "boundedContext.actors"], + { actors: this.getSummarizedActorValue( + esValue, boundedContext, keysToFilter, esAliasTransManager + )} + ), + ...getConditionalValue( + ["aggregates", "boundedContext.aggregates"], + { aggregates: boundedContext.aggregates + .map(aggregate => esValue.elements[aggregate.id]) + .map(aggregate => this.getSummarizedAggregateValue( + esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager + )) + } + ) + } + } + + static getSummarizedActorValue(esValue, boundedContext, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + const getUniqueActors = (actors, property) => { + let uniqueActors = [] + for(let actor of actors){ + if(!uniqueActors.some(a => a[property] === actor[property])) + uniqueActors.push(actor) + } + return uniqueActors + } + + + let actors = [] + for(let element of Object.values(esValue.elements)){ + if(element && (element._type === 'org.uengine.modeling.model.Actor') && + (element.boundedContext.id === boundedContext.id)){ + actors.push({ + ...getConditionalValue( + ["id", "actors.id"], + { id: this._getElementIdSafely(element, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "actors.name"], + { name: element.name } + ) + }) + } + } + + + if(!this._checkKeyFilters(keysToFilter, ["name", "actors.name"])) + return getUniqueActors(actors, "name") + + if(!this._checkKeyFilters(keysToFilter, ["id", "actors.id"])) + return getUniqueActors(actors, "id") + + return actors + } + + static getSummarizedAggregateValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + return { + ...getConditionalValue( + ["id", "aggregate.id"], + { id: this._getElementIdSafely(aggregate, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "aggregate.name"], + { name: aggregate.name } + ), + ...getConditionalValue( + ["properties", "aggregate.properties"], + { properties: this._getSummarizedFieldDescriptors(aggregate.aggregateRoot.fieldDescriptors) } + ), + ...getConditionalValue( + ["entities", "aggregate.entities"], + { entities: this.getSummarizedEntityValue(aggregate, keysToFilter, esAliasTransManager) } + ), + ...getConditionalValue( + ["enumerations", "aggregate.enumerations"], + { enumerations: this.getSummarizedEnumerationValue(aggregate, keysToFilter, esAliasTransManager) } + ), + ...getConditionalValue( + ["valueObjects", "aggregate.valueObjects"], + { valueObjects: this.getSummarizedValueObjectValue(aggregate, keysToFilter, esAliasTransManager) } + ), + ...getConditionalValue( + ["commands", "aggregate.commands"], + { commands: this.getSummarizedCommandValue(esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager) } + ), + ...getConditionalValue( + ["events", "aggregate.events"], + { events: this.getSummarizedEventValue(esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager) } + ), + ...getConditionalValue( + ["readModels", "aggregate.readModels"], + { readModels: this.getSummarizedReadModelValue(esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager) } + ) + } + } + + static getSummarizedEntityValue(aggregate, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + + if(!this._isAggregateHaveElements(aggregate)) return [] + + let summarizedEntityValue = [] + for(let element of Object.values(aggregate.aggregateRoot.entities.elements)) { + if(element && !element.isAggregateRoot && + (element._type === 'org.uengine.uml.model.Class')) { + summarizedEntityValue.push({ + ...getConditionalValue( + ["id", "entities.id"], + { id: this._getElementIdSafely(element, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "entities.name"], + { name: element.name } + ), + ...getConditionalValue( + ["properties", "entities.properties"], + { properties: this._getSummarizedFieldDescriptors( + element.fieldDescriptors, + (property, fieldDescriptor) => { + if(fieldDescriptor.className.toLowerCase() === aggregate.name.toLowerCase()) + property.isForeignProperty = true + } + )} + ) + }) + } + } + return summarizedEntityValue + } + + static getSummarizedEnumerationValue(aggregate, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + + if(!this._isAggregateHaveElements(aggregate)) return [] + + let summarizedEnumerationValue = [] + for(let element of Object.values(aggregate.aggregateRoot.entities.elements)) { + if(element && (element._type === 'org.uengine.uml.model.enum')) { + summarizedEnumerationValue.push({ + ...getConditionalValue( + ["id", "enumerations.id"], + { id: this._getElementIdSafely(element, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "enumerations.name"], + { name: element.name } + ), + ...getConditionalValue( + ["items", "enumerations.items"], + { items: element.items.map(item => item.value) } + ) + }) + } + } + return summarizedEnumerationValue + } + + static getSummarizedValueObjectValue(aggregate, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + if(!this._isAggregateHaveElements(aggregate)) return [] + + let summarizedValueObjectValue = [] + for(let element of Object.values(aggregate.aggregateRoot.entities.elements)) { + if(element && (element._type === 'org.uengine.uml.model.vo.Class')) { + summarizedValueObjectValue.push({ + ...getConditionalValue( + ["id", "valueObjects.id"], + { id: this._getElementIdSafely(element, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "valueObjects.name"], + { name: element.name } + ), + ...getConditionalValue( + ["properties", "valueObjects.properties"], + { properties: this._getSummarizedFieldDescriptors( + element.fieldDescriptors, + (property, fieldDescriptor) => { + if(fieldDescriptor.className.toLowerCase() === aggregate.name.toLowerCase()) + property.isForeignProperty = true + + if(fieldDescriptor.referenceClass) { + property.referencedAggregateName = fieldDescriptor.referenceClass + property.isForeignProperty = true + } + } + )} + ) + }) + } + } + return summarizedValueObjectValue + } + + static getSummarizedCommandValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + const getOutputEvents = (element) => { + let events = [] + for(let relation of Object.values(esValue.relations)) { + if(relation && relation.sourceElement.id === element.id && + relation.targetElement._type === 'org.uengine.modeling.model.Event') { + events.push({ + ...getConditionalValue( + ["id", "commands.outputEvents.id"], + { id: this._getElementIdSafely(relation.targetElement, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "commands.outputEvents.name"], + { name: relation.targetElement.name } + ) + }) + } + } + return events + } + + let summarizedCommandValue = [] + for(let element of Object.values(esValue.elements)) { + if(element && (element._type === 'org.uengine.modeling.model.Command') && + (element.boundedContext.id === boundedContext.id) && + (element.aggregate.id === aggregate.id)) { + + summarizedCommandValue.push({ + ...getConditionalValue( + ["id", "commands.id"], + { id: this._getElementIdSafely(element, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "commands.name"], + { name: element.name } + ), + ...getConditionalValue( + ["api_verb", "commands.api_verb"], + { api_verb: (element.restRepositoryInfo && element.restRepositoryInfo.method) + ? element.restRepositoryInfo.method + : "POST" + } + ), + ...getConditionalValue( + ["isRestRepository", "commands.isRestRepository"], + { isRestRepository: element.isRestRepository ? true : false } + ), + ...getConditionalValue( + ["properties", "commands.properties"], + { properties: element.fieldDescriptors ? + this._getSummarizedFieldDescriptors(element.fieldDescriptors) : [] + } + ), + ...getConditionalValue( + ["outputEvents", "commands.outputEvents"], + { outputEvents: getOutputEvents(element) } + ) + }) + } + } + return summarizedCommandValue + } + + static getSummarizedEventValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + const getRelationsForType = (esValue, sourceElement, targetType) => { + return Object.values(esValue.relations) + .filter(r => r && r.sourceElement.id === sourceElement.id && r.targetElement._type === targetType) + } + + const getOutputCommands = (element) => { + let commands = [] + for(let policyRelation of getRelationsForType(esValue, element, "org.uengine.modeling.model.Policy")) { + const targetPolicy = esValue.elements[policyRelation.targetElement.id] + if(!targetPolicy) continue + + for(let commandRelation of getRelationsForType(esValue, targetPolicy, "org.uengine.modeling.model.Command")) { + commands.push({ + ...getConditionalValue( + ["id", "events.outputCommands.id"], + { id: this._getElementIdSafely(commandRelation.targetElement, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "events.outputCommands.name"], + { name: commandRelation.targetElement.name } + ), + ...getConditionalValue( + ["id", "events.outputCommands.policyId"], + { policyId: this._getElementIdSafely(targetPolicy, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "events.outputCommands.policyName"], + { policyName: targetPolicy.name } + ) + }) + } + } + return commands + } + + let summarizedEventValue = [] + for(let element of Object.values(esValue.elements)) { + if(element && (element._type === 'org.uengine.modeling.model.Event') && + (element.boundedContext.id === boundedContext.id) && + (element.aggregate.id === aggregate.id)) { + + summarizedEventValue.push({ + ...getConditionalValue( + ["id", "events.id"], + { id: this._getElementIdSafely(element, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "events.name"], + { name: element.name } + ), + ...getConditionalValue( + ["properties", "events.properties"], + { properties: element.fieldDescriptors ? + this._getSummarizedFieldDescriptors(element.fieldDescriptors) : [] + } + ), + ...getConditionalValue( + ["outputCommands", "events.outputCommands"], + { outputCommands: getOutputCommands(element) } + ) + }) + } + } + return summarizedEventValue + } + + static getSummarizedReadModelValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { + const getConditionalValue = (keys, value) => { + return !this._checkKeyFilters(keysToFilter, keys) ? value : {} + } + + let summarizedReadModelValue = [] + for(let element of Object.values(esValue.elements)) { + if(element && (element._type === "org.uengine.modeling.model.View") && + (element.boundedContext.id === boundedContext.id) && + (element.aggregate.id === aggregate.id)) { + + summarizedReadModelValue.push({ + ...getConditionalValue( + ["id", "readModels.id"], + { id: this._getElementIdSafely(element, esAliasTransManager) } + ), + ...getConditionalValue( + ["name", "readModels.name"], + { name: element.name } + ), + ...getConditionalValue( + ["properties", "readModels.properties"], + { properties: element.queryParameters ? + this._getSummarizedFieldDescriptors(element.queryParameters) : [] + } // ReadModel인 경우에만 기존과 다르게 queryParameters로 참조 + ), + ...getConditionalValue( + ["isMultipleResult", "readModels.isMultipleResult"], + { isMultipleResult: element.isMultipleResult ? true : false } + ) + }) + } + } + return summarizedReadModelValue + } + + + /** + * 주어진 BoundedContext에서 agggregates 속성이 없을 경우를 대비해서, 주어진 이벤트스토밍 값을 통해서 복원 + */ + static _restoreBoundedContextAggregatesProperties(esValue, boundedContext) { + boundedContext.aggregates = [] + for(let element of Object.values(esValue.elements)) + { + if(element && element._type === "org.uengine.modeling.model.Aggregate" + && element.boundedContext && element.boundedContext.id === boundedContext.id) + boundedContext.aggregates.push({id: element.id}) + } + } + + static _getSummarizedFieldDescriptors(fieldDescriptors, onAfterCreateProperty=null) { + return fieldDescriptors.map(fieldDescriptor => { + let property = { + name: fieldDescriptor.name + } + + if(!(fieldDescriptor.className.toLowerCase().includes("string"))) + property.type = fieldDescriptor.className + + if(fieldDescriptor.isKey) + property.isKey = true + + if(onAfterCreateProperty) onAfterCreateProperty(property, fieldDescriptor) + return property + }) + } + + static _checkKeyFilters(keysToFilter, valuesToCheck, onNotMatch=null) { + for(let key of keysToFilter) + if(valuesToCheck.includes(key)) return true + + if(onNotMatch) onNotMatch() + return false + } + + static _getElementIdSafely(element, esAliasTransManager=null) { + if(esAliasTransManager) return esAliasTransManager.getElementAliasSafely(element) + if(element.id) return element.id + if(element.elementView) return element.elementView.id + throw new Error("전달된 이벤트 스토밍 엘리먼트중에서 id를 구할 수 없는 객체가 존재함! " + element) + } + + static _isAggregateHaveElements(aggregate) { + return aggregate.aggregateRoot && aggregate.aggregateRoot.entities && aggregate.aggregateRoot.entities.elements + } +} + +ESValueSummarizeWithFilter.KEY_FILTER_TEMPLATES = { + "aggregateOuterStickers": ["aggregate.commands", "aggregate.events", "aggregate.readModels"], + "aggregateInnerStickers": ["aggregate.entities", "aggregate.enumerations", "aggregate.valueObjects"], + "detailedProperties": ["properties", "items"] +} + +module.exports = ESValueSummarizeWithFilter \ No newline at end of file diff --git a/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/index.js b/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/index.js index bbe822f..eaafc44 100644 --- a/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/index.js +++ b/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/index.js @@ -1,654 +1 @@ -const { TokenCounter } = require("../../../utils") - -class ESValueSummarizeWithFilter { - /** - * getSummarizedESValue()에서 반환될 값에 대한 상세한 입력 프롬프트 가이드 반환 - */ - static getGuidePrompt() { - return `You will receive a JSON object containing summarized information about the event storming model on which you will perform your task. Not all guided properties may be passed, and properties that are unnecessary for the task may be excluded. -The approximate structure is as follows. -{ - // Properties that have been deleted because they are not needed for the given task. - "deletedProperties": [""], - - "boundedContexts": [ - "id": "", - "name": "", - "actors": [ - { - "id": "", - "name": "" - } - ], - "aggregates": [ - { - "id": "", - "name": "", - "properties": [ - { - "name": "", - - // "" must belong to one of the following three categories: - // 1. Well-known Java class names. In this case, write only the class name without the full package path. (e.g., java.lang.String > String) - // 2. One of the following values: Address, Photo, User, Money, Email, Payment, Weather, File, Likes, Tags, Comment - // 3. If there's a name defined in Enumerations or ValueObjects or Entities, It can be used as the property type. - // If the type is String, do not specify the type. - ["type": ""], - - // Only one of the properties should have isKey set to true. - // If it needs a composite key, it will reference a ValueObject with those properties. - ["isKey": true] - } - ], - - "entities": [ - { - "id": "", - "name": "", - "properties": [ - { - "name": "", - ["type": ""], - ["isKey": true], - - // If this property references a property in another table, this value should be set to true. - ["isForeignProperty": true] - } - ] - } - ], - - "enumerations": [ - { - "id": "", - "name": "", - "items": [""] - } - ], - - "valueObjects": [ - { - "id": "", - "name": "", - "properties": [ - { - "name": "", - ["type": ""], - ["isKey": true], - ["isForeignProperty": true], - - // If the property is used as a foreign key to reference another Aggregate, write the name of that Aggregate. - ["referencedAggregateName": ""] - } - ] - } - ], - - "commands": [ - { - "id": "", - "name": "", - "api_verb": <"POST" | "PUT" | "PATCH" | "DELETE">, - - // Determines the API endpoint structure: - // true: Uses standard REST endpoints with HTTP verbs only (e.g., POST /book) - // false: Uses custom endpoints with command names for complex operations (e.g., POST /book/updateStatus) - "isRestRepository": , - "properties": [ - { - "name": "", - ["type": ""], - ["isKey": true] - } - ], - - // A list of cascading events that occur when a command is executed. - "outputEvents": [ - { - "id": "", - "name": "" - } - ] - } - ], - - "events": [ - { - "id": "", - "name": "", - "properties": [ - { - "name": "", - ["type": ""], - ["isKey": true] - } - ], - - // A list of cascading commands that occur when an event is executed. - "outputCommands": [ - { - "id": "", - "name": "", - "policyId": "", - "policyName": "" - } - ] - } - ], - - "readModels": [ - { - "id": "", - "name": "", - "properties": [ - { - "name": "", - ["type": ""], - ["isKey": true] - } - ], - "isMultipleResult": - } - ] - } - ] - ] -}` - } - - static async getSummarizedESValueWithMaxTokenSummarize(esValue, keysToFilter=[], esAliasTransManager=null, maxTokens=800, tokenCalcModel="gpt-4o"){ - const summarizedESValue = this.getSummarizedESValue( - esValue, keysToFilter, esAliasTransManager - ) - - const tokenCount = TokenCounter.getTokenCount(JSON.stringify(summarizedESValue), tokenCalcModel) - if(tokenCount < maxTokens) - return summarizedESValue - - throw new Error("토큰 수가 초과되었습니다.") - } - - /** - * 주어진 이벤트스토밍에서 위치 데이터등을 제외한 핵심 정보들만 추출해서 LLM에게 입력 데이터로 제공 - * keysToFilter: 일부 경우에는 properties와 같은 구체적인 속성들이 불필요할 수 있음. 이런 경우에 제외시킬 키값을 배열로 전달 - * ex) id: 모든 id 속성을 미포함, boundedContext.id: boundedContext의 id 속성을 미포함 - * esAliasTransManager: Id값을 별칭으로 바꿔서 UUID를 제거해서, 패턴 기반의 LLM의 성능을 향상 - */ - static getSummarizedESValue(esValue, keysToFilter=[], esAliasTransManager=null){ - const boundedContexts = Object.values(esValue.elements) - .filter(element => element && element._type === 'org.uengine.modeling.model.BoundedContext') - - let summarizedBoundedContexts = boundedContexts.map(boundedContext => - this.getSummarizedBoundedContextValue( - esValue, boundedContext, keysToFilter, esAliasTransManager - ) - ) - - return { - deletedProperties: keysToFilter, - boundedContexts: summarizedBoundedContexts - } - } - - - static getSummarizedBoundedContextValue(esValue, boundedContext, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - - this._restoreBoundedContextAggregatesProperties(esValue, boundedContext) - - return { - ...getConditionalValue( - ["id", "boundedContext.id"], - { id: this._getElementIdSafely(boundedContext, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "boundedContext.name"], - { name: boundedContext.name } - ), - ...getConditionalValue( - ["actors", "boundedContext.actors"], - { actors: this.getSummarizedActorValue( - esValue, boundedContext, keysToFilter, esAliasTransManager - )} - ), - ...getConditionalValue( - ["aggregates", "boundedContext.aggregates"], - { aggregates: boundedContext.aggregates - .map(aggregate => esValue.elements[aggregate.id]) - .map(aggregate => this.getSummarizedAggregateValue( - esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager - )) - } - ) - } - } - - static getSummarizedActorValue(esValue, boundedContext, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - const getUniqueActors = (actors, property) => { - let uniqueActors = [] - for(let actor of actors){ - if(!uniqueActors.some(a => a[property] === actor[property])) - uniqueActors.push(actor) - } - return uniqueActors - } - - - let actors = [] - for(let element of Object.values(esValue.elements)){ - if(element && (element._type === 'org.uengine.modeling.model.Actor') && - (element.boundedContext.id === boundedContext.id)){ - actors.push({ - ...getConditionalValue( - ["id", "actors.id"], - { id: this._getElementIdSafely(element, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "actors.name"], - { name: element.name } - ) - }) - } - } - - - if(!this._checkKeyFilters(keysToFilter, ["name", "actors.name"])) - return getUniqueActors(actors, "name") - - if(!this._checkKeyFilters(keysToFilter, ["id", "actors.id"])) - return getUniqueActors(actors, "id") - - return actors - } - - static getSummarizedAggregateValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - return { - ...getConditionalValue( - ["id", "aggregate.id"], - { id: this._getElementIdSafely(aggregate, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "aggregate.name"], - { name: aggregate.name } - ), - ...getConditionalValue( - ["properties", "aggregate.properties"], - { properties: this._getSummarizedFieldDescriptors(aggregate.aggregateRoot.fieldDescriptors) } - ), - ...getConditionalValue( - ["entities", "aggregate.entities"], - { entities: this.getSummarizedEntityValue(aggregate, keysToFilter, esAliasTransManager) } - ), - ...getConditionalValue( - ["enumerations", "aggregate.enumerations"], - { enumerations: this.getSummarizedEnumerationValue(aggregate, keysToFilter, esAliasTransManager) } - ), - ...getConditionalValue( - ["valueObjects", "aggregate.valueObjects"], - { valueObjects: this.getSummarizedValueObjectValue(aggregate, keysToFilter, esAliasTransManager) } - ), - ...getConditionalValue( - ["commands", "aggregate.commands"], - { commands: this.getSummarizedCommandValue(esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager) } - ), - ...getConditionalValue( - ["events", "aggregate.events"], - { events: this.getSummarizedEventValue(esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager) } - ), - ...getConditionalValue( - ["readModels", "aggregate.readModels"], - { readModels: this.getSummarizedReadModelValue(esValue, boundedContext, aggregate, keysToFilter, esAliasTransManager) } - ) - } - } - - static getSummarizedEntityValue(aggregate, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - - if(!this._isAggregateHaveElements(aggregate)) return [] - - let summarizedEntityValue = [] - for(let element of Object.values(aggregate.aggregateRoot.entities.elements)) { - if(element && !element.isAggregateRoot && - (element._type === 'org.uengine.uml.model.Class')) { - summarizedEntityValue.push({ - ...getConditionalValue( - ["id", "entities.id"], - { id: this._getElementIdSafely(element, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "entities.name"], - { name: element.name } - ), - ...getConditionalValue( - ["properties", "entities.properties"], - { properties: this._getSummarizedFieldDescriptors( - element.fieldDescriptors, - (property, fieldDescriptor) => { - if(fieldDescriptor.className.toLowerCase() === aggregate.name.toLowerCase()) - property.isForeignProperty = true - } - )} - ) - }) - } - } - return summarizedEntityValue - } - - static getSummarizedEnumerationValue(aggregate, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - - if(!this._isAggregateHaveElements(aggregate)) return [] - - let summarizedEnumerationValue = [] - for(let element of Object.values(aggregate.aggregateRoot.entities.elements)) { - if(element && (element._type === 'org.uengine.uml.model.enum')) { - summarizedEnumerationValue.push({ - ...getConditionalValue( - ["id", "enumerations.id"], - { id: this._getElementIdSafely(element, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "enumerations.name"], - { name: element.name } - ), - ...getConditionalValue( - ["items", "enumerations.items"], - { items: element.items.map(item => item.value) } - ) - }) - } - } - return summarizedEnumerationValue - } - - static getSummarizedValueObjectValue(aggregate, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - if(!this._isAggregateHaveElements(aggregate)) return [] - - let summarizedValueObjectValue = [] - for(let element of Object.values(aggregate.aggregateRoot.entities.elements)) { - if(element && (element._type === 'org.uengine.uml.model.vo.Class')) { - summarizedValueObjectValue.push({ - ...getConditionalValue( - ["id", "valueObjects.id"], - { id: this._getElementIdSafely(element, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "valueObjects.name"], - { name: element.name } - ), - ...getConditionalValue( - ["properties", "valueObjects.properties"], - { properties: this._getSummarizedFieldDescriptors( - element.fieldDescriptors, - (property, fieldDescriptor) => { - if(fieldDescriptor.className.toLowerCase() === aggregate.name.toLowerCase()) - property.isForeignProperty = true - - if(fieldDescriptor.referenceClass) { - property.referencedAggregateName = fieldDescriptor.referenceClass - property.isForeignProperty = true - } - } - )} - ) - }) - } - } - return summarizedValueObjectValue - } - - static getSummarizedCommandValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - const getOutputEvents = (element) => { - let events = [] - for(let relation of Object.values(esValue.relations)) { - if(relation && relation.sourceElement.id === element.id && - relation.targetElement._type === 'org.uengine.modeling.model.Event') { - events.push({ - ...getConditionalValue( - ["id", "commands.outputEvents.id"], - { id: this._getElementIdSafely(relation.targetElement, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "commands.outputEvents.name"], - { name: relation.targetElement.name } - ) - }) - } - } - return events - } - - let summarizedCommandValue = [] - for(let element of Object.values(esValue.elements)) { - if(element && (element._type === 'org.uengine.modeling.model.Command') && - (element.boundedContext.id === boundedContext.id) && - (element.aggregate.id === aggregate.id)) { - - summarizedCommandValue.push({ - ...getConditionalValue( - ["id", "commands.id"], - { id: this._getElementIdSafely(element, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "commands.name"], - { name: element.name } - ), - ...getConditionalValue( - ["api_verb", "commands.api_verb"], - { api_verb: (element.restRepositoryInfo && element.restRepositoryInfo.method) - ? element.restRepositoryInfo.method - : "POST" - } - ), - ...getConditionalValue( - ["isRestRepository", "commands.isRestRepository"], - { isRestRepository: element.isRestRepository ? true : false } - ), - ...getConditionalValue( - ["properties", "commands.properties"], - { properties: element.fieldDescriptors ? - this._getSummarizedFieldDescriptors(element.fieldDescriptors) : [] - } - ), - ...getConditionalValue( - ["outputEvents", "commands.outputEvents"], - { outputEvents: getOutputEvents(element) } - ) - }) - } - } - return summarizedCommandValue - } - - static getSummarizedEventValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - const getRelationsForType = (esValue, sourceElement, targetType) => { - return Object.values(esValue.relations) - .filter(r => r && r.sourceElement.id === sourceElement.id && r.targetElement._type === targetType) - } - - const getOutputCommands = (element) => { - let commands = [] - for(let policyRelation of getRelationsForType(esValue, element, "org.uengine.modeling.model.Policy")) { - const targetPolicy = esValue.elements[policyRelation.targetElement.id] - if(!targetPolicy) continue - - for(let commandRelation of getRelationsForType(esValue, targetPolicy, "org.uengine.modeling.model.Command")) { - commands.push({ - ...getConditionalValue( - ["id", "events.outputCommands.id"], - { id: this._getElementIdSafely(commandRelation.targetElement, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "events.outputCommands.name"], - { name: commandRelation.targetElement.name } - ), - ...getConditionalValue( - ["id", "events.outputCommands.policyId"], - { policyId: this._getElementIdSafely(targetPolicy, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "events.outputCommands.policyName"], - { policyName: targetPolicy.name } - ) - }) - } - } - return commands - } - - let summarizedEventValue = [] - for(let element of Object.values(esValue.elements)) { - if(element && (element._type === 'org.uengine.modeling.model.Event') && - (element.boundedContext.id === boundedContext.id) && - (element.aggregate.id === aggregate.id)) { - - summarizedEventValue.push({ - ...getConditionalValue( - ["id", "events.id"], - { id: this._getElementIdSafely(element, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "events.name"], - { name: element.name } - ), - ...getConditionalValue( - ["properties", "events.properties"], - { properties: element.fieldDescriptors ? - this._getSummarizedFieldDescriptors(element.fieldDescriptors) : [] - } - ), - ...getConditionalValue( - ["outputCommands", "events.outputCommands"], - { outputCommands: getOutputCommands(element) } - ) - }) - } - } - return summarizedEventValue - } - - static getSummarizedReadModelValue(esValue, boundedContext, aggregate, keysToFilter=[], esAliasTransManager=null) { - const getConditionalValue = (keys, value) => { - return !this._checkKeyFilters(keysToFilter, keys) ? value : {} - } - - let summarizedReadModelValue = [] - for(let element of Object.values(esValue.elements)) { - if(element && (element._type === "org.uengine.modeling.model.View") && - (element.boundedContext.id === boundedContext.id) && - (element.aggregate.id === aggregate.id)) { - - summarizedReadModelValue.push({ - ...getConditionalValue( - ["id", "readModels.id"], - { id: this._getElementIdSafely(element, esAliasTransManager) } - ), - ...getConditionalValue( - ["name", "readModels.name"], - { name: element.name } - ), - ...getConditionalValue( - ["properties", "readModels.properties"], - { properties: element.queryParameters ? - this._getSummarizedFieldDescriptors(element.queryParameters) : [] - } // ReadModel인 경우에만 기존과 다르게 queryParameters로 참조 - ), - ...getConditionalValue( - ["isMultipleResult", "readModels.isMultipleResult"], - { isMultipleResult: element.isMultipleResult ? true : false } - ) - }) - } - } - return summarizedReadModelValue - } - - - /** - * 주어진 BoundedContext에서 agggregates 속성이 없을 경우를 대비해서, 주어진 이벤트스토밍 값을 통해서 복원 - */ - static _restoreBoundedContextAggregatesProperties(esValue, boundedContext) { - boundedContext.aggregates = [] - for(let element of Object.values(esValue.elements)) - { - if(element && element._type === "org.uengine.modeling.model.Aggregate" - && element.boundedContext && element.boundedContext.id === boundedContext.id) - boundedContext.aggregates.push({id: element.id}) - } - } - - static _getSummarizedFieldDescriptors(fieldDescriptors, onAfterCreateProperty=null) { - return fieldDescriptors.map(fieldDescriptor => { - let property = { - name: fieldDescriptor.name - } - - if(!(fieldDescriptor.className.toLowerCase().includes("string"))) - property.type = fieldDescriptor.className - - if(fieldDescriptor.isKey) - property.isKey = true - - if(onAfterCreateProperty) onAfterCreateProperty(property, fieldDescriptor) - return property - }) - } - - static _checkKeyFilters(keysToFilter, valuesToCheck, onNotMatch=null) { - for(let key of keysToFilter) - if(valuesToCheck.includes(key)) return true - - if(onNotMatch) onNotMatch() - return false - } - - static _getElementIdSafely(element, esAliasTransManager=null) { - if(esAliasTransManager) return esAliasTransManager.getElementAliasSafely(element) - if(element.id) return element.id - if(element.elementView) return element.elementView.id - throw new Error("전달된 이벤트 스토밍 엘리먼트중에서 id를 구할 수 없는 객체가 존재함! " + element) - } - - static _isAggregateHaveElements(aggregate) { - return aggregate.aggregateRoot && aggregate.aggregateRoot.entities && aggregate.aggregateRoot.entities.elements - } -} - -ESValueSummarizeWithFilter.KEY_FILTER_TEMPLATES = { - "aggregateOuterStickers": ["aggregate.commands", "aggregate.events", "aggregate.readModels"], - "aggregateInnerStickers": ["aggregate.entities", "aggregate.enumerations", "aggregate.valueObjects"], - "detailedProperties": ["properties", "items"] -} - -module.exports = ESValueSummarizeWithFilter \ No newline at end of file +export const ESValueSummarizeWithFilter = require("./ESValueSummarizeWithFilter"); \ No newline at end of file diff --git a/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/tests.js b/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/tests.js index e383aa5..93ed263 100644 --- a/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/tests.js +++ b/src/components/designer/modeling/generators/es-generators/helpers/ESValueSummarizeWithFilter/tests.js @@ -1,6 +1,6 @@ const libraryEsValue = require("./mocks") const ESAliasTransManager = require("../../../es-ddl-generators/modules/ESAliasTransManager") -const ESValueSummarizeWithFilter = require("./index") +const ESValueSummarizeWithFilter = require("./ESValueSummarizeWithFilter") class ESValueSummarizeWithFilterTest { static async test(esValue=null) { diff --git a/src/components/designer/modeling/generators/es-generators/helpers/index.js b/src/components/designer/modeling/generators/es-generators/helpers/index.js index c61bfab..ffa6fe2 100644 --- a/src/components/designer/modeling/generators/es-generators/helpers/index.js +++ b/src/components/designer/modeling/generators/es-generators/helpers/index.js @@ -1 +1 @@ -export const ESValueSummarizeWithFilter = require("./ESValueSummarizeWithFilter"); +export const { ESValueSummarizeWithFilter } = require("./ESValueSummarizeWithFilter"); diff --git a/src/components/designer/modeling/generators/es-generators/index.js b/src/components/designer/modeling/generators/es-generators/index.js index e01b7d0..6f135ea 100644 --- a/src/components/designer/modeling/generators/es-generators/index.js +++ b/src/components/designer/modeling/generators/es-generators/index.js @@ -1 +1 @@ -export const ESValueSummaryGenerator = require("./ESValueSummaryGenerator"); \ No newline at end of file +export const { ESValueSummaryGenerator } = require("./ESValueSummaryGenerator"); \ No newline at end of file diff --git a/src/components/designer/modeling/generators/utils/TokenCounter/TokenCounter.js b/src/components/designer/modeling/generators/utils/TokenCounter/TokenCounter.js new file mode 100644 index 0000000..05897f1 --- /dev/null +++ b/src/components/designer/modeling/generators/utils/TokenCounter/TokenCounter.js @@ -0,0 +1,400 @@ +const { encoderMap, defaultEncoder } = require("./constants"); + +/** + * @description AI 모델별 텍스트 토큰화 및 토큰 수 관리를 위한 유틸리티 클래스입니다. + * 다양한 AI 모델(GPT-4, GPT-3.5 등)에 대한 토큰 수 계산, 텍스트 분할, + * 토큰 제한 관리 등의 기능을 제공합니다. + * + * @class + * + * @property {Object} encoderMap - AI 모델별 토큰화 인코더 매핑 + * - key: 모델 패턴 (정규식 문자열) + * - value: 해당 모델의 토큰화 인코더 + * + * @throws {Error} 텍스트 인코딩 과정에서 오류 발생 시 + * @throws {Error} 잘못된 입력값이 제공된 경우 + * @throws {Error} 토큰 분할 시 중복 크기가 청크 크기보다 큰 경우 + * + * @see o200k_base - 기본 토큰화 인코더 + * @see cl100k_base - GPT-4/3.5 토큰화 인코더 + * @see p50k_base - GPT-3 토큰화 인코더 + * + * @example 기본 토큰 수 계산 + * // 단일 텍스트의 토큰 수 계산 + * const text = "안녕하세요, AI의 세계에 오신 것을 환영합니다!"; + * const tokenCount = TokenCounter.getTokenCount(text, "gpt-4o"); + * console.log(tokenCount); // 예상 출력: 13 + * + * @example 텍스트 분할 및 토큰 제한 관리 + * // 긴 텍스트를 토큰 제한에 맞게 분할 + * const longText = "긴 문서의 내용..."; + * const chunks = TokenCounter.splitByTokenLimit(longText, "gpt-3.5-turbo", 1000, 100); + * + * // 토큰 제한 확인 + * const isWithin = TokenCounter.isWithinTokenLimit(text, "gpt-4o", 2000); + * + * // 토큰 제한에 맞게 텍스트 자르기 + * const truncated = TokenCounter.truncateToTokenLimit(text, "gpt-4o", 50, { + * addEllipsis: true, + * preserveSentences: true + * }); + * + * @note + * - 모든 메서드는 정적(static) 메서드로 제공됩니다 + * - 같은 텍스트도 AI 모델에 따라 토큰 수가 다를 수 있습니다 + * - 토큰 수 계산 시 항상 정확한 모델명을 지정해야 합니다 + * - 대용량 텍스트 처리 시 메모리 사용량에 주의가 필요합니다 + * - 텍스트 분할 시 문맥 유지를 위해 오버랩 기능을 활용할 수 있습니다 + */ +class TokenCounter { + /** + * @description 주어진 텍스트의 토큰 수를 계산하는 메서드입니다. + * AI 모델별로 서로 다른 토큰화 방식을 사용하여 정확한 토큰 수를 계산합니다. + * 토큰 수 제한이 있는 API 호출 전에 텍스트의 토큰 수를 미리 확인하는 데 활용할 수 있습니다. + * + * @param {string} text - 토큰 수를 계산할 텍스트 + * - 빈 문자열도 유효한 입력으로 처리됩니다 + * @param {string} model - 사용할 AI 모델명 + * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 + * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 + * + * @returns {number} 계산된 토큰 수 + * + * @throws {Error} 텍스트 인코딩 과정에서 오류가 발생한 경우 + * + * @see TokenCounter.getTotalTokenCount - 여러 텍스트의 총 토큰 수 계산 + * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 + * @see TokenCounter._getEncoderForModel - 모델별 인코더 선택 + * + * @example 기본 토큰 수 계산 + * const tokenCount = TokenCounter.getTokenCount("Hello, World!", "gpt-4o"); + * console.log(tokenCount); // 예: 4 + * + * @example 다양한 모델에 대한 토큰 수 계산 + * const text = "AI is transforming the world"; + * console.log(TokenCounter.getTokenCount(text, "gpt-4o")); // GPT-4 모델 기준 + * console.log(TokenCounter.getTokenCount(text, "gpt-3.5-turbo")); // GPT-3 모델 기준 + * + * @note + * - 같은 텍스트라도 모델에 따라 토큰 수가 다를 수 있습니다 + * - 정확한 토큰 수 계산을 위해 올바른 모델명을 지정하는 것이 중요합니다 + */ + static getTokenCount(text, model) { + try { + const encoder = this._getEncoderForModel(model); + return encoder.encode(text).length; + } catch (error) { + console.error(`Error counting tokens: ${error.message}`); + throw error; + } + } + + /** + * @description 여러 텍스트의 총 토큰 수를 계산하는 메서드입니다. + * 채팅 메시지나 문서 청크와 같이 여러 텍스트의 결합된 토큰 수를 확인할 때 유용합니다. + * + * @param {Array} texts - 토큰 수를 계산할 텍스트 배열 + * - 각 요소는 비어있지 않은 문자열이어야 합니다 + * - 빈 배열도 유효한 입력으로 처리됩니다 + * @param {string} model - 사용할 AI 모델명 + * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 + * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 + * + * @returns {number} 모든 텍스트의 총 토큰 수 + * + * @throws {Error} 텍스트 인코딩 과정에서 오류가 발생한 경우 + * @throws {TypeError} texts가 배열이 아니거나, 배열 요소가 문자열이 아닌 경우 + * + * @see TokenCounter.getTokenCount - 단일 텍스트의 토큰 수 계산 + * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 + * + * @example 채팅 메시지의 총 토큰 수 계산 + * const messages = [ + * "안녕하세요!", + * "오늘 날씨가 좋네요.", + * "산책하기 좋은 날입니다." + * ]; + * const totalTokens = TokenCounter.getTotalTokenCount(messages, "gpt-4o"); + * console.log(totalTokens); // 예: 25 + * + * @example 문서 청크의 토큰 수 계산 + * const documentChunks = [ + * "첫 번째 문단입니다.", + * "두 번째 문단의 내용입니다.", + * "마지막 문단이 됩니다." + * ]; + * console.log(TokenCounter.getTotalTokenCount(documentChunks, "gpt-3.5-turbo")); // 예: 30 + * + * @note + * - 총 토큰 수는 각 텍스트의 토큰 수의 합으로 계산됩니다 + * - 모델에 따라 같은 텍스트도 다른 토큰 수를 가질 수 있습니다 + * - 대량의 텍스트를 처리할 때는 메모리 사용량에 주의해야 합니다 + */ + static getTotalTokenCount(texts, model) { + return texts.reduce((total, text) => total + this.getTokenCount(text, model), 0); + } + + /** + * @description 주어진 텍스트가 지정된 토큰 제한을 초과하지 않는지 확인하는 메서드입니다. + * API 호출이나 텍스트 처리 전에 토큰 제한을 미리 확인하는 데 활용할 수 있습니다. + * + * @param {string} text - 토큰 수를 확인할 텍스트 + * - 빈 문자열도 유효한 입력으로 처리됩니다 + * @param {string} model - 사용할 AI 모델명 + * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 + * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 + * @param {number} maxTokens - 최대 허용 토큰 수 + * - 양의 정수여야 합니다 + * + * @returns {boolean} 토큰 수가 제한 이내이면 true, 초과하면 false + * + * @throws {Error} 텍스트 인코딩 과정에서 오류가 발생한 경우 + * + * @see TokenCounter.getTokenCount - 토큰 수 계산 + * @see TokenCounter.truncateToTokenLimit - 토큰 제한에 맞게 텍스트 자르기 + * @see TokenCounter.splitByTokenLimit - 토큰 제한에 맞게 텍스트 분할 + * + * @example 기본 토큰 제한 확인 + * const text = "안녕하세요, AI의 세계에 오신 것을 환영합니다!"; + * const isWithin = TokenCounter.isWithinTokenLimit(text, "gpt-4o", 10); + * console.log(isWithin); // false + * + * @example API 호출 전 토큰 제한 확인 + * const prompt = "긴 프롬프트 텍스트..."; + * if (TokenCounter.isWithinTokenLimit(prompt, "gpt-3.5-turbo", 4096)) { + * // API 호출 진행 + * } else { + * // 텍스트 축소 또는 분할 처리 + * } + * + * @note + * - 같은 텍스트라도 모델에 따라 토큰 수가 다를 수 있으므로 정확한 모델 지정이 중요합니다 + * - 토큰 제한 초과 시 truncateToTokenLimit 또는 splitByTokenLimit 메서드 사용을 고려하세요 + */ + static isWithinTokenLimit(text, model, maxTokens) { + return this.getTokenCount(text, model) <= maxTokens; + } + + /** + * @description 긴 텍스트를 지정된 토큰 수로 나누어 청크(chunks)로 분할하는 메서드입니다. + * 대량의 텍스트를 AI 모델의 토큰 제한에 맞게 처리할 때 유용하며, + * 오버랩 기능을 통해 청크 간의 문맥을 유지할 수 있습니다. + * + * @param {string} text - 분할할 텍스트 + * - 빈 문자열도 유효한 입력으로 처리됩니다 + * @param {string} model - 사용할 AI 모델명 + * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 + * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 + * @param {number} maxTokensPerChunk - 각 청크의 최대 토큰 수 + * - 양의 정수여야 합니다 + * @param {number} [overlap=0] - 연속된 청크 간에 중복될 토큰 수 + * - 문맥 유지를 위해 청크 간에 중복되는 토큰 수를 지정 + * - maxTokensPerChunk보다 작아야 합니다 + * + * @returns {Array} 분할된 텍스트 청크들의 배열 + * + * @throws {Error} overlap이 maxTokensPerChunk보다 크거나 같은 경우 + * + * @see TokenCounter.getTokenCount - 토큰 수 계산 + * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 + * @see TokenCounter.truncateToTokenLimit - 토큰 제한에 맞게 텍스트 자르기 + * + * @example 기본 텍스트 분할 + * const longText = "긴 문서 내용..."; + * const chunks = TokenCounter.splitByTokenLimit(longText, "gpt-4o", 1000); + * chunks.forEach(chunk => { + * // 각 청크 처리 + * console.log(chunk); + * }); + * + * @example 오버랩을 사용한 문맥 유지 분할 + * const text = "복잡한 기술 문서..."; + * const chunks = TokenCounter.splitByTokenLimit(text, "gpt-3.5-turbo", 2000, 200); + * // 각 청크는 이전 청크와 200토큰이 중복되어 문맥 유지 + * + * @note + * - 오버랩을 사용하면 청크 간의 문맥은 유지되지만 총 토큰 사용량이 증가합니다 + * - 마지막 청크는 maxTokensPerChunk보다 작을 수 있습니다 + * - 모델별로 토큰화 방식이 다르므로, 동일한 텍스트도 모델에 따라 다르게 분할될 수 있습니다 + */ + static splitByTokenLimit(text, model, maxTokensPerChunk, overlap = 0) { + if (overlap >= maxTokensPerChunk) { + throw new Error('Overlap size must be less than maxTokensPerChunk'); + } + + const encoder = this._getEncoderForModel(model); + const tokens = encoder.encode(text); + const chunks = []; + + const step = maxTokensPerChunk - overlap; + + for (let i = 0; i < tokens.length; i += step) { + const chunkTokens = tokens.slice(i, i + maxTokensPerChunk); + chunks.push(encoder.decode(chunkTokens)); + } + + return chunks; + } + + /** + * @description 긴 텍스트를 지정된 토큰 제한에 맞게 잘라내는 메서드입니다. + * 텍스트의 끝에 생략 부호를 추가하고, 문장 단위로 자르는 등의 옵션을 제공합니다. + * AI 모델의 토큰 제한을 준수하면서 자연스러운 텍스트 처리가 필요할 때 사용합니다. + * + * @param {string} text - 잘라낼 텍스트 + * - 비어있지 않은 문자열이어야 합니다 + * @param {string} model - 사용할 AI 모델명 + * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 + * @param {number} maxTokens - 최대 허용 토큰 수 + * - 1 이상의 정수여야 합니다 + * @param {Object} [options] - 텍스트 자르기 옵션 + * @param {boolean} [options.addEllipsis=true] - 잘린 텍스트 끝에 '...' 추가 여부 + * @param {boolean} [options.preserveSentences=true] - 문장 단위로 자르기 여부 + * @param {boolean} [options.debug=false] - 디버그 정보 출력 여부 + * + * @returns {string} 토큰 제한에 맞게 잘린 텍스트 + * + * @throws {Error} text가 유효하지 않은 경우 + * @throws {Error} maxTokens가 1 미만인 경우 + * + * @see TokenCounter.getTokenCount - 토큰 수 계산 + * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 + * @see TokenCounter.splitByTokenLimit - 텍스트를 여러 청크로 분할 + * + * @example 기본 텍스트 자르기 + * const longText = "이것은 매우 긴 텍스트입니다. 문장이 계속 이어집니다. 끝이 어디일까요?"; + * const truncated = TokenCounter.truncateToTokenLimit(longText, "gpt-4o", 10); + * console.log(truncated); + * // 출력: "이것은 매우 긴..." + * // 토큰 수: 10 (원본: 25) + * + * @example 고급 옵션 활용 + * const text = "첫 번째 문장입니다. 두 번째 문장입니다. 세 번째 문장입니다."; + * + * // 1. 기본 옵션 (문장 보존 + 생략 부호) + * console.log(TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15)); + * // 출력: "첫 번째 문장입니다. 두 번째 문장입니다..." + * + * // 2. 문장 보존 없이 자르기 + * console.log(TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15, { + * preserveSentences: false + * })); + * // 출력: "첫 번째 문장입니다. 두 번..." + * + * // 3. 생략 부호 없이 문장 단위로 자르기 + * console.log(TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15, { + * addEllipsis: false, + * preserveSentences: true + * })); + * // 출력: "첫 번째 문장입니다. 두 번째 문장입니다." + * + * // 4. 디버그 모드 활용 + * TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15, { + * debug: true + * }); + * // 콘솔 출력: + * // { + * // originalLength: 28, + * // truncatedLength: 15, + * // maxTokens: 15, + * // preservedSentences: true, + * // hasEllipsis: true + * // } + * + * @note + * - preserveSentences가 true일 때는 문장이 중간에 잘리지 않습니다 + * - addEllipsis 옵션 사용 시 '...'의 토큰 수도 maxTokens에 포함됩니다 + * - debug 모드에서는 원본 길이, 잘린 길이 등의 상세 정보를 확인할 수 있습니다 + */ + static truncateToTokenLimit(text, model, maxTokens, options = {}) { + const { + addEllipsis = true, + preserveSentences = true, + debug = false + } = options; + + + if (!text || typeof text !== 'string') { + throw new Error('Invalid input: text must be a non-empty string'); + } + if (maxTokens < 1) { + throw new Error('maxTokens must be a positive number'); + } + + const encoder = this._getEncoderForModel(model); + const tokens = encoder.encode(text); + + if (tokens.length <= maxTokens) { + return text; + } + + + const ellipsisTokens = addEllipsis ? encoder.encode('...').length : 0; + const effectiveMaxTokens = maxTokens - ellipsisTokens; + + let truncatedTokens = tokens.slice(0, effectiveMaxTokens); + let truncatedText = encoder.decode(truncatedTokens); + + + if (preserveSentences) { + const sentenceEndRegex = /[.!?][^.!?]*$/; + const lastSentenceMatch = truncatedText.match(sentenceEndRegex); + if (lastSentenceMatch) { + truncatedText = truncatedText.slice(0, lastSentenceMatch.index + 1); + } + } + + if (addEllipsis) { + truncatedText += '...'; + } + + + if (debug) { + console.log({ + originalLength: tokens.length, + truncatedLength: encoder.encode(truncatedText).length, + maxTokens, + preservedSentences: preserveSentences, + hasEllipsis: addEllipsis + }); + } + + return truncatedText; + } + + + static _getEncoderForModel(model) { + let encoderName = defaultEncoder; + let matched = false; + + for (const [pattern, name] of Object.entries(encoderMap)) { + if (new RegExp(pattern, 'i').test(model)) { + encoderName = name; + matched = true; + break; + } + } + + if (!matched) { + console.warn(`Warning: Unknown model "${model}". Using default o200k_base encoder.`); + } + + + if (this._encoderCache.has(encoderName)) { + return this._encoderCache.get(encoderName); + } + + try { + const encoder = require(`./encoders/${encoderName}.legacy`); + this._encoderCache.set(encoderName, encoder); + return encoder; + } catch (error) { + console.error(`Failed to load encoder ${encoderName}:`, error); + throw error; + } + } +} + +TokenCounter._encoderCache = new Map(); + +module.exports = TokenCounter; \ No newline at end of file diff --git a/src/components/designer/modeling/generators/utils/TokenCounter/index.js b/src/components/designer/modeling/generators/utils/TokenCounter/index.js index 05897f1..1283ce3 100644 --- a/src/components/designer/modeling/generators/utils/TokenCounter/index.js +++ b/src/components/designer/modeling/generators/utils/TokenCounter/index.js @@ -1,400 +1 @@ -const { encoderMap, defaultEncoder } = require("./constants"); - -/** - * @description AI 모델별 텍스트 토큰화 및 토큰 수 관리를 위한 유틸리티 클래스입니다. - * 다양한 AI 모델(GPT-4, GPT-3.5 등)에 대한 토큰 수 계산, 텍스트 분할, - * 토큰 제한 관리 등의 기능을 제공합니다. - * - * @class - * - * @property {Object} encoderMap - AI 모델별 토큰화 인코더 매핑 - * - key: 모델 패턴 (정규식 문자열) - * - value: 해당 모델의 토큰화 인코더 - * - * @throws {Error} 텍스트 인코딩 과정에서 오류 발생 시 - * @throws {Error} 잘못된 입력값이 제공된 경우 - * @throws {Error} 토큰 분할 시 중복 크기가 청크 크기보다 큰 경우 - * - * @see o200k_base - 기본 토큰화 인코더 - * @see cl100k_base - GPT-4/3.5 토큰화 인코더 - * @see p50k_base - GPT-3 토큰화 인코더 - * - * @example 기본 토큰 수 계산 - * // 단일 텍스트의 토큰 수 계산 - * const text = "안녕하세요, AI의 세계에 오신 것을 환영합니다!"; - * const tokenCount = TokenCounter.getTokenCount(text, "gpt-4o"); - * console.log(tokenCount); // 예상 출력: 13 - * - * @example 텍스트 분할 및 토큰 제한 관리 - * // 긴 텍스트를 토큰 제한에 맞게 분할 - * const longText = "긴 문서의 내용..."; - * const chunks = TokenCounter.splitByTokenLimit(longText, "gpt-3.5-turbo", 1000, 100); - * - * // 토큰 제한 확인 - * const isWithin = TokenCounter.isWithinTokenLimit(text, "gpt-4o", 2000); - * - * // 토큰 제한에 맞게 텍스트 자르기 - * const truncated = TokenCounter.truncateToTokenLimit(text, "gpt-4o", 50, { - * addEllipsis: true, - * preserveSentences: true - * }); - * - * @note - * - 모든 메서드는 정적(static) 메서드로 제공됩니다 - * - 같은 텍스트도 AI 모델에 따라 토큰 수가 다를 수 있습니다 - * - 토큰 수 계산 시 항상 정확한 모델명을 지정해야 합니다 - * - 대용량 텍스트 처리 시 메모리 사용량에 주의가 필요합니다 - * - 텍스트 분할 시 문맥 유지를 위해 오버랩 기능을 활용할 수 있습니다 - */ -class TokenCounter { - /** - * @description 주어진 텍스트의 토큰 수를 계산하는 메서드입니다. - * AI 모델별로 서로 다른 토큰화 방식을 사용하여 정확한 토큰 수를 계산합니다. - * 토큰 수 제한이 있는 API 호출 전에 텍스트의 토큰 수를 미리 확인하는 데 활용할 수 있습니다. - * - * @param {string} text - 토큰 수를 계산할 텍스트 - * - 빈 문자열도 유효한 입력으로 처리됩니다 - * @param {string} model - 사용할 AI 모델명 - * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 - * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 - * - * @returns {number} 계산된 토큰 수 - * - * @throws {Error} 텍스트 인코딩 과정에서 오류가 발생한 경우 - * - * @see TokenCounter.getTotalTokenCount - 여러 텍스트의 총 토큰 수 계산 - * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 - * @see TokenCounter._getEncoderForModel - 모델별 인코더 선택 - * - * @example 기본 토큰 수 계산 - * const tokenCount = TokenCounter.getTokenCount("Hello, World!", "gpt-4o"); - * console.log(tokenCount); // 예: 4 - * - * @example 다양한 모델에 대한 토큰 수 계산 - * const text = "AI is transforming the world"; - * console.log(TokenCounter.getTokenCount(text, "gpt-4o")); // GPT-4 모델 기준 - * console.log(TokenCounter.getTokenCount(text, "gpt-3.5-turbo")); // GPT-3 모델 기준 - * - * @note - * - 같은 텍스트라도 모델에 따라 토큰 수가 다를 수 있습니다 - * - 정확한 토큰 수 계산을 위해 올바른 모델명을 지정하는 것이 중요합니다 - */ - static getTokenCount(text, model) { - try { - const encoder = this._getEncoderForModel(model); - return encoder.encode(text).length; - } catch (error) { - console.error(`Error counting tokens: ${error.message}`); - throw error; - } - } - - /** - * @description 여러 텍스트의 총 토큰 수를 계산하는 메서드입니다. - * 채팅 메시지나 문서 청크와 같이 여러 텍스트의 결합된 토큰 수를 확인할 때 유용합니다. - * - * @param {Array} texts - 토큰 수를 계산할 텍스트 배열 - * - 각 요소는 비어있지 않은 문자열이어야 합니다 - * - 빈 배열도 유효한 입력으로 처리됩니다 - * @param {string} model - 사용할 AI 모델명 - * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 - * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 - * - * @returns {number} 모든 텍스트의 총 토큰 수 - * - * @throws {Error} 텍스트 인코딩 과정에서 오류가 발생한 경우 - * @throws {TypeError} texts가 배열이 아니거나, 배열 요소가 문자열이 아닌 경우 - * - * @see TokenCounter.getTokenCount - 단일 텍스트의 토큰 수 계산 - * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 - * - * @example 채팅 메시지의 총 토큰 수 계산 - * const messages = [ - * "안녕하세요!", - * "오늘 날씨가 좋네요.", - * "산책하기 좋은 날입니다." - * ]; - * const totalTokens = TokenCounter.getTotalTokenCount(messages, "gpt-4o"); - * console.log(totalTokens); // 예: 25 - * - * @example 문서 청크의 토큰 수 계산 - * const documentChunks = [ - * "첫 번째 문단입니다.", - * "두 번째 문단의 내용입니다.", - * "마지막 문단이 됩니다." - * ]; - * console.log(TokenCounter.getTotalTokenCount(documentChunks, "gpt-3.5-turbo")); // 예: 30 - * - * @note - * - 총 토큰 수는 각 텍스트의 토큰 수의 합으로 계산됩니다 - * - 모델에 따라 같은 텍스트도 다른 토큰 수를 가질 수 있습니다 - * - 대량의 텍스트를 처리할 때는 메모리 사용량에 주의해야 합니다 - */ - static getTotalTokenCount(texts, model) { - return texts.reduce((total, text) => total + this.getTokenCount(text, model), 0); - } - - /** - * @description 주어진 텍스트가 지정된 토큰 제한을 초과하지 않는지 확인하는 메서드입니다. - * API 호출이나 텍스트 처리 전에 토큰 제한을 미리 확인하는 데 활용할 수 있습니다. - * - * @param {string} text - 토큰 수를 확인할 텍스트 - * - 빈 문자열도 유효한 입력으로 처리됩니다 - * @param {string} model - 사용할 AI 모델명 - * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 - * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 - * @param {number} maxTokens - 최대 허용 토큰 수 - * - 양의 정수여야 합니다 - * - * @returns {boolean} 토큰 수가 제한 이내이면 true, 초과하면 false - * - * @throws {Error} 텍스트 인코딩 과정에서 오류가 발생한 경우 - * - * @see TokenCounter.getTokenCount - 토큰 수 계산 - * @see TokenCounter.truncateToTokenLimit - 토큰 제한에 맞게 텍스트 자르기 - * @see TokenCounter.splitByTokenLimit - 토큰 제한에 맞게 텍스트 분할 - * - * @example 기본 토큰 제한 확인 - * const text = "안녕하세요, AI의 세계에 오신 것을 환영합니다!"; - * const isWithin = TokenCounter.isWithinTokenLimit(text, "gpt-4o", 10); - * console.log(isWithin); // false - * - * @example API 호출 전 토큰 제한 확인 - * const prompt = "긴 프롬프트 텍스트..."; - * if (TokenCounter.isWithinTokenLimit(prompt, "gpt-3.5-turbo", 4096)) { - * // API 호출 진행 - * } else { - * // 텍스트 축소 또는 분할 처리 - * } - * - * @note - * - 같은 텍스트라도 모델에 따라 토큰 수가 다를 수 있으므로 정확한 모델 지정이 중요합니다 - * - 토큰 제한 초과 시 truncateToTokenLimit 또는 splitByTokenLimit 메서드 사용을 고려하세요 - */ - static isWithinTokenLimit(text, model, maxTokens) { - return this.getTokenCount(text, model) <= maxTokens; - } - - /** - * @description 긴 텍스트를 지정된 토큰 수로 나누어 청크(chunks)로 분할하는 메서드입니다. - * 대량의 텍스트를 AI 모델의 토큰 제한에 맞게 처리할 때 유용하며, - * 오버랩 기능을 통해 청크 간의 문맥을 유지할 수 있습니다. - * - * @param {string} text - 분할할 텍스트 - * - 빈 문자열도 유효한 입력으로 처리됩니다 - * @param {string} model - 사용할 AI 모델명 - * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 - * - 알 수 없는 모델의 경우 기본 o200k_base 인코더가 사용됨 - * @param {number} maxTokensPerChunk - 각 청크의 최대 토큰 수 - * - 양의 정수여야 합니다 - * @param {number} [overlap=0] - 연속된 청크 간에 중복될 토큰 수 - * - 문맥 유지를 위해 청크 간에 중복되는 토큰 수를 지정 - * - maxTokensPerChunk보다 작아야 합니다 - * - * @returns {Array} 분할된 텍스트 청크들의 배열 - * - * @throws {Error} overlap이 maxTokensPerChunk보다 크거나 같은 경우 - * - * @see TokenCounter.getTokenCount - 토큰 수 계산 - * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 - * @see TokenCounter.truncateToTokenLimit - 토큰 제한에 맞게 텍스트 자르기 - * - * @example 기본 텍스트 분할 - * const longText = "긴 문서 내용..."; - * const chunks = TokenCounter.splitByTokenLimit(longText, "gpt-4o", 1000); - * chunks.forEach(chunk => { - * // 각 청크 처리 - * console.log(chunk); - * }); - * - * @example 오버랩을 사용한 문맥 유지 분할 - * const text = "복잡한 기술 문서..."; - * const chunks = TokenCounter.splitByTokenLimit(text, "gpt-3.5-turbo", 2000, 200); - * // 각 청크는 이전 청크와 200토큰이 중복되어 문맥 유지 - * - * @note - * - 오버랩을 사용하면 청크 간의 문맥은 유지되지만 총 토큰 사용량이 증가합니다 - * - 마지막 청크는 maxTokensPerChunk보다 작을 수 있습니다 - * - 모델별로 토큰화 방식이 다르므로, 동일한 텍스트도 모델에 따라 다르게 분할될 수 있습니다 - */ - static splitByTokenLimit(text, model, maxTokensPerChunk, overlap = 0) { - if (overlap >= maxTokensPerChunk) { - throw new Error('Overlap size must be less than maxTokensPerChunk'); - } - - const encoder = this._getEncoderForModel(model); - const tokens = encoder.encode(text); - const chunks = []; - - const step = maxTokensPerChunk - overlap; - - for (let i = 0; i < tokens.length; i += step) { - const chunkTokens = tokens.slice(i, i + maxTokensPerChunk); - chunks.push(encoder.decode(chunkTokens)); - } - - return chunks; - } - - /** - * @description 긴 텍스트를 지정된 토큰 제한에 맞게 잘라내는 메서드입니다. - * 텍스트의 끝에 생략 부호를 추가하고, 문장 단위로 자르는 등의 옵션을 제공합니다. - * AI 모델의 토큰 제한을 준수하면서 자연스러운 텍스트 처리가 필요할 때 사용합니다. - * - * @param {string} text - 잘라낼 텍스트 - * - 비어있지 않은 문자열이어야 합니다 - * @param {string} model - 사용할 AI 모델명 - * - 지원 모델: gpt-4o, o1, gpt-4, gpt-3.5, gpt-3, text-davinci-002/003 등 - * @param {number} maxTokens - 최대 허용 토큰 수 - * - 1 이상의 정수여야 합니다 - * @param {Object} [options] - 텍스트 자르기 옵션 - * @param {boolean} [options.addEllipsis=true] - 잘린 텍스트 끝에 '...' 추가 여부 - * @param {boolean} [options.preserveSentences=true] - 문장 단위로 자르기 여부 - * @param {boolean} [options.debug=false] - 디버그 정보 출력 여부 - * - * @returns {string} 토큰 제한에 맞게 잘린 텍스트 - * - * @throws {Error} text가 유효하지 않은 경우 - * @throws {Error} maxTokens가 1 미만인 경우 - * - * @see TokenCounter.getTokenCount - 토큰 수 계산 - * @see TokenCounter.isWithinTokenLimit - 토큰 제한 확인 - * @see TokenCounter.splitByTokenLimit - 텍스트를 여러 청크로 분할 - * - * @example 기본 텍스트 자르기 - * const longText = "이것은 매우 긴 텍스트입니다. 문장이 계속 이어집니다. 끝이 어디일까요?"; - * const truncated = TokenCounter.truncateToTokenLimit(longText, "gpt-4o", 10); - * console.log(truncated); - * // 출력: "이것은 매우 긴..." - * // 토큰 수: 10 (원본: 25) - * - * @example 고급 옵션 활용 - * const text = "첫 번째 문장입니다. 두 번째 문장입니다. 세 번째 문장입니다."; - * - * // 1. 기본 옵션 (문장 보존 + 생략 부호) - * console.log(TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15)); - * // 출력: "첫 번째 문장입니다. 두 번째 문장입니다..." - * - * // 2. 문장 보존 없이 자르기 - * console.log(TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15, { - * preserveSentences: false - * })); - * // 출력: "첫 번째 문장입니다. 두 번..." - * - * // 3. 생략 부호 없이 문장 단위로 자르기 - * console.log(TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15, { - * addEllipsis: false, - * preserveSentences: true - * })); - * // 출력: "첫 번째 문장입니다. 두 번째 문장입니다." - * - * // 4. 디버그 모드 활용 - * TokenCounter.truncateToTokenLimit(text, "gpt-3.5-turbo", 15, { - * debug: true - * }); - * // 콘솔 출력: - * // { - * // originalLength: 28, - * // truncatedLength: 15, - * // maxTokens: 15, - * // preservedSentences: true, - * // hasEllipsis: true - * // } - * - * @note - * - preserveSentences가 true일 때는 문장이 중간에 잘리지 않습니다 - * - addEllipsis 옵션 사용 시 '...'의 토큰 수도 maxTokens에 포함됩니다 - * - debug 모드에서는 원본 길이, 잘린 길이 등의 상세 정보를 확인할 수 있습니다 - */ - static truncateToTokenLimit(text, model, maxTokens, options = {}) { - const { - addEllipsis = true, - preserveSentences = true, - debug = false - } = options; - - - if (!text || typeof text !== 'string') { - throw new Error('Invalid input: text must be a non-empty string'); - } - if (maxTokens < 1) { - throw new Error('maxTokens must be a positive number'); - } - - const encoder = this._getEncoderForModel(model); - const tokens = encoder.encode(text); - - if (tokens.length <= maxTokens) { - return text; - } - - - const ellipsisTokens = addEllipsis ? encoder.encode('...').length : 0; - const effectiveMaxTokens = maxTokens - ellipsisTokens; - - let truncatedTokens = tokens.slice(0, effectiveMaxTokens); - let truncatedText = encoder.decode(truncatedTokens); - - - if (preserveSentences) { - const sentenceEndRegex = /[.!?][^.!?]*$/; - const lastSentenceMatch = truncatedText.match(sentenceEndRegex); - if (lastSentenceMatch) { - truncatedText = truncatedText.slice(0, lastSentenceMatch.index + 1); - } - } - - if (addEllipsis) { - truncatedText += '...'; - } - - - if (debug) { - console.log({ - originalLength: tokens.length, - truncatedLength: encoder.encode(truncatedText).length, - maxTokens, - preservedSentences: preserveSentences, - hasEllipsis: addEllipsis - }); - } - - return truncatedText; - } - - - static _getEncoderForModel(model) { - let encoderName = defaultEncoder; - let matched = false; - - for (const [pattern, name] of Object.entries(encoderMap)) { - if (new RegExp(pattern, 'i').test(model)) { - encoderName = name; - matched = true; - break; - } - } - - if (!matched) { - console.warn(`Warning: Unknown model "${model}". Using default o200k_base encoder.`); - } - - - if (this._encoderCache.has(encoderName)) { - return this._encoderCache.get(encoderName); - } - - try { - const encoder = require(`./encoders/${encoderName}.legacy`); - this._encoderCache.set(encoderName, encoder); - return encoder; - } catch (error) { - console.error(`Failed to load encoder ${encoderName}:`, error); - throw error; - } - } -} - -TokenCounter._encoderCache = new Map(); - -module.exports = TokenCounter; \ No newline at end of file +export const TokenCounter = require("./TokenCounter"); \ No newline at end of file diff --git a/src/components/designer/modeling/generators/utils/TokenCounter/tests.js b/src/components/designer/modeling/generators/utils/TokenCounter/tests.js index 5916dd6..e7f1a9a 100644 --- a/src/components/designer/modeling/generators/utils/TokenCounter/tests.js +++ b/src/components/designer/modeling/generators/utils/TokenCounter/tests.js @@ -1,4 +1,4 @@ -const TokenCounter = require("./index"); +const TokenCounter = require("./TokenCounter"); const { sentences, sentence, longText } = require("./mocks"); class TokenCounterTest { diff --git a/src/components/designer/modeling/generators/utils/index.js b/src/components/designer/modeling/generators/utils/index.js index abf607b..066c396 100644 --- a/src/components/designer/modeling/generators/utils/index.js +++ b/src/components/designer/modeling/generators/utils/index.js @@ -1 +1 @@ -export const TokenCounter = require("./TokenCounter"); +export const { TokenCounter } = require("./TokenCounter");