UNPKG

@autobe/agent

Version:

AI backend server code generator

268 lines (244 loc) 8.53 kB
import { IAgenticaController } from "@agentica/core"; import { AutoBeEventSource, AutoBeInterfaceSchemaRefactor, AutoBeInterfaceSchemaRenameEvent, AutoBeOpenApi, AutoBeProgressEventBase, } from "@autobe/interface"; import { OpenApiTypeChecker } from "@typia/utils"; import { IPointer, Singleton } from "tstl"; import typia, { ILlmApplication, OpenApi } from "typia"; import { v7 } from "uuid"; import { AutoBeContext } from "../../context/AutoBeContext"; import { divideArray } from "../../utils/divideArray"; import { executeCachedBatch } from "../../utils/executeCachedBatch"; import { transformInterfaceSchemaRenameHistory } from "./histories/transformInterfaceSchemaRenameHistory"; import { IAutoBeInterfaceSchemaRenameApplication } from "./structures/IAutoBeInterfaceSchemaRenameApplication"; import { AutoBeJsonSchemaCollection } from "./utils/AutoBeJsonSchemaCollection"; import { AutoBeJsonSchemaFactory } from "./utils/AutoBeJsonSchemaFactory"; export async function orchestrateInterfaceSchemaRename( ctx: AutoBeContext, props: { operations: AutoBeOpenApi.IOperation[]; progress: AutoBeProgressEventBase; collection: AutoBeJsonSchemaCollection; }, capacity: number = 25, ): Promise<void> { const tableNames: string[] = ctx .state() .database!.result.data.files.map((f) => f.models) .flat() .map((m) => m.name) .filter((m) => m.startsWith("mv_") === false); const entireTypeNames: Set<string> = new Set(); const insert = (key: string) => { if (key.startsWith("IPage")) key = key.replace("IPage", ""); key = key.split(".")[0]; entireTypeNames.add(key); }; for (const op of props.operations) { if (op.requestBody) insert(op.requestBody.typeName); if (op.responseBody) insert(op.responseBody.typeName); } for (const key of Object.keys(props.collection.schemas)) insert(key); const matrix: string[][] = divideArray({ array: Array.from(entireTypeNames), capacity, }); props.progress.total += matrix.length; const refactors: AutoBeInterfaceSchemaRefactor[] = uniqueRefactors( ( await executeCachedBatch( ctx, matrix.map( (typeNames) => (promptCacheKey) => divideAndConquer(ctx, { tableNames, typeNames, promptCacheKey, progress: props.progress, }), ), ) ).flat(), ); orchestrateInterfaceSchemaRename.rename({ operations: props.operations, collection: props.collection, refactors, }); } export namespace orchestrateInterfaceSchemaRename { export const rename = (props: { operations: AutoBeOpenApi.IOperation[]; collection: AutoBeJsonSchemaCollection; refactors: AutoBeInterfaceSchemaRefactor[]; }): void => { // REPLACE RULE const replace = (typeName: string): string | null => { // exact match const exact: AutoBeInterfaceSchemaRefactor | undefined = props.refactors.find((r) => r.from === typeName); if (exact !== undefined) return exact.to; // T.X match const prefix: AutoBeInterfaceSchemaRefactor | undefined = props.refactors.find((r) => typeName.startsWith(`${r.from}.`)); if (prefix !== undefined) return typeName.replace(`${prefix.from}.`, `${prefix.to}.`); // IPageT exact match const pageExact: AutoBeInterfaceSchemaRefactor | undefined = props.refactors.find((r) => typeName === `IPage${r.from}`); if (pageExact !== undefined) return `IPage${pageExact.to}`; // IPageT.X match const pagePrefix: AutoBeInterfaceSchemaRefactor | undefined = props.refactors.find((r) => typeName.startsWith(`IPage${r.from}.`)); if (pagePrefix !== undefined) return typeName.replace( `IPage${pagePrefix.from}.`, `IPage${pagePrefix.to}.`, ); return null; }; // JSON SCHEMA REFERENCES const $refChangers: Map<OpenApi.IJsonSchema, () => void> = new Map(); for (const value of Object.values(props.collection.schemas)) OpenApiTypeChecker.visit({ components: { schemas: props.collection.schemas }, schema: value, closure: (schema) => { if (OpenApiTypeChecker.isReference(schema) === false) return; const x: string = schema.$ref.split("/").pop()!; const y: string | null = replace(x); if (y !== null) $refChangers.set(schema, () => { schema.$ref = `#/components/schemas/${y}`; }); }, }); for (const fn of $refChangers.values()) fn(); // COMPONENT SCHEMAS for (const x of Object.keys(props.collection.schemas)) { const y: string | null = replace(x); if (y !== null && x !== y) { props.collection.set(y, props.collection.get(x)!); props.collection.delete(x); } } // OPERATIONS for (const op of props.operations) { if (op.requestBody) op.requestBody.typeName = replace(op.requestBody.typeName) ?? op.requestBody.typeName; if (op.responseBody) op.responseBody.typeName = replace(op.responseBody.typeName) ?? op.responseBody.typeName; } }; } const divideAndConquer = async ( ctx: AutoBeContext, props: { tableNames: string[]; typeNames: string[]; promptCacheKey: string; progress: AutoBeProgressEventBase; }, ): Promise<AutoBeInterfaceSchemaRefactor[]> => { const counter = new Singleton(() => ++props.progress.completed); try { const pointer: IPointer<IAutoBeInterfaceSchemaRenameApplication.IProps | null> = { value: null, }; const { metric, tokenUsage } = await ctx.conversate({ source: SOURCE, controller: createController((value) => (pointer.value = value)), enforceFunctionCall: true, promptCacheKey: props.promptCacheKey, ...transformInterfaceSchemaRenameHistory(props), }); if (pointer.value === null) { counter.get(); return []; } pointer.value.refactors = uniqueRefactors(pointer.value.refactors); ctx.dispatch({ type: SOURCE, id: v7(), refactors: pointer.value.refactors, total: props.progress.total, completed: counter.get(), metric, tokenUsage, created_at: new Date().toISOString(), } satisfies AutoBeInterfaceSchemaRenameEvent); return pointer.value.refactors; } catch { counter.get(); return []; } }; const uniqueRefactors = ( refactors: AutoBeInterfaceSchemaRefactor[], ): AutoBeInterfaceSchemaRefactor[] => { // Remove self-references (A->A) refactors = refactors.filter((r) => r.from !== r.to); // Remove presets refactors = refactors.filter( (r) => AutoBeJsonSchemaFactory.DEFAULT_SCHEMAS[r.from] === undefined, ); // Remove duplicates (keep the first occurrence) refactors = Array.from(new Map(refactors.map((r) => [r.from, r])).values()); // Build adjacency map: from -> to const renameMap: Map<string, string> = new Map(); for (const r of refactors) { renameMap.set(r.from, r.to); } // Resolve transitive chains: A->B, B->C becomes A->C const resolveChain = (from: string): string => { const visited: Set<string> = new Set(); let current: string = from; while (renameMap.has(current)) { // Cycle detection: A->B, B->C, C->A if (visited.has(current)) { // Cycle detected, keep the last valid mapping before cycle return current; } visited.add(current); current = renameMap.get(current)!; } return current; }; // Build final refactor list with resolved chains const resolved: Map<string, AutoBeInterfaceSchemaRefactor> = new Map(); for (const from of renameMap.keys()) { const finalTo: string = resolveChain(from); // Only include if actually changes if (from !== finalTo) { resolved.set(from, { from, to: finalTo, }); } } return Array.from(resolved.values()); }; const createController = ( build: (value: IAutoBeInterfaceSchemaRenameApplication.IProps) => void, ): IAgenticaController.IClass => { const application: ILlmApplication = typia.llm.application<IAutoBeInterfaceSchemaRenameApplication>(); return { protocol: "class", name: SOURCE, application, execute: { rename: (props) => { build(props); }, } satisfies IAutoBeInterfaceSchemaRenameApplication, }; }; const SOURCE = "interfaceSchemaRename" satisfies AutoBeEventSource;