@autobe/agent
Version:
AI backend server code generator
268 lines (244 loc) • 8.53 kB
text/typescript
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;