@autobe/agent
Version:
AI backend server code generator
293 lines (281 loc) • 9.13 kB
text/typescript
import { IAgenticaController, MicroAgentica } from "@agentica/core";
import { AutoBeOpenApi } from "@autobe/interface";
import { ILlmApplication, ILlmSchema } from "@samchon/openapi";
import { OpenApiV3_1Emender } from "@samchon/openapi/lib/converters/OpenApiV3_1Emender";
import { IPointer } from "tstl";
import typia from "typia";
import { v4 } from "uuid";
import { AutoBeSystemPromptConstant } from "../../constants/AutoBeSystemPromptConstant";
import { AutoBeContext } from "../../context/AutoBeContext";
import { assertSchemaModel } from "../../context/assertSchemaModel";
import { divideArray } from "../../utils/divideArray";
import { enforceToolCall } from "../../utils/enforceToolCall";
import { transformInterfaceHistories } from "./transformInterfaceHistories";
export async function orchestrateInterfaceComponents<
Model extends ILlmSchema.Model,
>(
ctx: AutoBeContext<Model>,
operations: AutoBeOpenApi.IOperation[],
capacity: number = 12,
): Promise<AutoBeOpenApi.IComponents> {
const typeNames: Set<string> = new Set();
for (const op of operations) {
if (op.requestBody !== null) typeNames.add(op.requestBody.typeName);
if (op.responseBody !== null) typeNames.add(op.responseBody.typeName);
}
const matrix: string[][] = divideArray({
array: Array.from(typeNames),
capacity,
});
let progress: number = 0;
const x: AutoBeOpenApi.IComponents = {
schemas: {},
};
for (const y of await Promise.all(
matrix.map(async (it) => {
const row: AutoBeOpenApi.IComponents = await divideAndConquer(
ctx,
operations,
it,
3,
(count) => {
progress += count;
},
);
ctx.dispatch({
type: "interfaceComponents",
components: row,
completed: progress,
total: typeNames.size,
step: ctx.state().analyze?.step ?? 0,
created_at: new Date().toISOString(),
});
return row;
}),
)) {
Object.assign(x.schemas, y.schemas);
if (y.authorization) x.authorization = y.authorization;
}
return x;
}
async function divideAndConquer<Model extends ILlmSchema.Model>(
ctx: AutoBeContext<Model>,
operations: AutoBeOpenApi.IOperation[],
typeNames: string[],
retry: number,
progress: (completed: number) => void,
): Promise<AutoBeOpenApi.IComponents> {
const remained: Set<string> = new Set(typeNames);
const components: AutoBeOpenApi.IComponents = {
schemas: {},
};
for (let i: number = 0; i < retry; ++i) {
if (remained.size === 0) break;
const before: number = remained.size;
const newbie: AutoBeOpenApi.IComponents = await process(
ctx,
operations,
components,
remained,
);
for (const key of Object.keys(newbie.schemas)) {
components.schemas[key] = newbie.schemas[key];
remained.delete(key);
}
if (before - remained.size !== 0) progress(before - remained.size);
}
return components;
}
async function process<Model extends ILlmSchema.Model>(
ctx: AutoBeContext<Model>,
operations: AutoBeOpenApi.IOperation[],
oldbie: AutoBeOpenApi.IComponents,
remained: Set<string>,
): Promise<AutoBeOpenApi.IComponents> {
const pointer: IPointer<AutoBeOpenApi.IComponents | null> = {
value: null,
};
const agentica: MicroAgentica<Model> = new MicroAgentica({
model: ctx.model,
vendor: ctx.vendor,
config: {
...(ctx.config ?? {}),
executor: {
describe: null,
},
},
histories: [
...transformInterfaceHistories(
ctx.state(),
AutoBeSystemPromptConstant.INTERFACE_SCHEMA,
),
{
id: v4(),
created_at: new Date().toISOString(),
type: "assistantMessage",
text: [
"Here is the OpenAPI operations generated by phase 2.",
"",
"```json",
JSON.stringify(operations),
"```",
].join("\n"),
},
],
tokenUsage: ctx.usage(),
controllers: [
createApplication({
model: ctx.model,
build: async (components) => {
pointer.value ??= {
schemas: {},
};
pointer.value.authorization ??= components.authorization;
Object.assign(pointer.value.schemas, components.schemas);
},
pointer,
}),
],
});
enforceToolCall(agentica);
const already: string[] = Object.keys(oldbie.schemas);
await agentica.conversate(
[
"Make type components please.",
"",
"Here is the list of request/response bodies' type names from",
"OpenAPI operations. Make type components of them. If more object",
"types are required during making the components, please make them",
"too.",
"",
...Array.from(remained).map((k) => `- \`${k}\``),
...(already.length !== 0
? [
"",
"> By the way, here is the list of components schemas what you've",
"> already made. So, you don't need to make them again.",
">",
...already.map((k) => `> - \`${k}\``),
]
: []),
].join("\n"),
);
if (pointer.value === null) {
// never be happened
throw new Error("Failed to create components.");
}
return OpenApiV3_1Emender.convertComponents(
pointer.value,
) as AutoBeOpenApi.IComponents;
}
function createApplication<Model extends ILlmSchema.Model>(props: {
model: Model;
build: (components: AutoBeOpenApi.IComponents) => Promise<void>;
pointer: IPointer<AutoBeOpenApi.IComponents | null>;
}): IAgenticaController.IClass<Model> {
assertSchemaModel(props.model);
const application: ILlmApplication<Model> = collection[
props.model
] as unknown as ILlmApplication<Model>;
return {
protocol: "class",
name: "interface",
application,
execute: {
makeComponents: async (next) => {
await props.build(next.components);
},
} satisfies IApplication,
};
}
const claude = typia.llm.application<
IApplication,
"claude",
{ reference: true }
>();
const collection = {
chatgpt: typia.llm.application<
IApplication,
"chatgpt",
{ reference: true }
>(),
claude,
llama: claude,
deepseek: claude,
"3.1": claude,
"3.0": typia.llm.application<IApplication, "3.0">(),
};
interface IApplication {
/**
* Generate OpenAPI components containing named schema types.
*
* This method receives a complete set of schema components and integrates
* them into the final OpenAPI specification. It processes all entity schemas,
* their variants, and related type definitions to ensure a comprehensive and
* consistent API design.
*
* The provided components should include schemas for all entities identified
* in the previous phases of API path/method definition and operation
* creation. This ensures that the final OpenAPI document has complete type
* coverage for all operations.
*
* CRITICAL: All schema definitions must follow the established naming
* conventions (IEntityName, IEntityName.ICreate, etc.) and must be thoroughly
* documented with descriptions that reference the original Prisma schema
* comments.
*
* @param props Properties containing components to generate.
*/
makeComponents(props: IMakeComponentsProps): void;
}
interface IMakeComponentsProps {
/**
* Complete set of schema components for the OpenAPI specification.
*
* This property contains comprehensive type definitions for all entities in
* the system. It is the central repository of all named schema types that
* will be used throughout the API specification.
*
* CRITICAL REQUIREMENT: All object types MUST be defined as named types in
* the components.schemas section. Inline anonymous object definitions are
* strictly prohibited.
*
* This components object should include:
*
* - Main entity types (IEntityName)
* - Operation-specific variants (.ICreate, .IUpdate, .ISummary, etc.)
* - Container types (IPage<T> for pagination)
* - Enumeration types
*
* All schema definitions must include detailed descriptions that reference
* the original Prisma schema comments and thoroughly document each property.
* Every property that references an object must use a $ref to a named type in
* the components.schemas section. This applies to all objects in request
* bodies, response bodies, and properties that are objects or arrays of
* objects.
*
* Example structure:
*
* ```typescript
* {
* components: {
* schemas: {
* IUser: {
* type: "object",
* properties: {
* id: { type: "string", format: "uuid" },
* email: { type: "string", format: "email" },
* profile: { "$ref": "#/components/schemas/IUserProfile" }
* },
* required: ["id", "email"],
* description: "User entity representing system account holders..."
* },
* "IUser.ICreate": { ... },
* // Additional schemas
* }
* }
* }
* ```
*/
components: AutoBeOpenApi.IComponents;
}