UNPKG

@autobe/agent

Version:

AI backend server code generator

317 lines (300 loc) 10.4 kB
import { IAgenticaController } from "@agentica/core"; import { AutoBeAnalyzeHistory, AutoBeEventSource, AutoBeInterfaceEndpointDesign, AutoBeInterfaceOperationEvent, AutoBeOpenApi, AutoBeProgressEventBase, } from "@autobe/interface"; import { AutoBeOpenApiEndpointComparator } from "@autobe/utils"; import { NamingConvention } from "@typia/utils"; import { HashMap, IPointer, Pair, Singleton } from "tstl"; import typia, { ILlmApplication, IValidation } from "typia"; import { v7 } from "uuid"; import { AutoBeContext } from "../../context/AutoBeContext"; import { buildAnalysisContextSections } from "../../utils/RAGRetrieval"; import { executeCachedBatch } from "../../utils/executeCachedBatch"; import { forceRetry } from "../../utils/forceRetry"; import { getEmbedder } from "../../utils/getEmbedder"; import { AutoBePreliminaryController } from "../common/AutoBePreliminaryController"; import { convertToSectionEntries } from "../common/internal/convertToSectionEntries"; import { IAnalysisSectionEntry } from "../common/structures/IAnalysisSectionEntry"; import { transformInterfaceOperationHistory } from "./histories/transformInterfaceOperationHistory"; import { AutoBeInterfaceAuthorizationProgrammer } from "./programmers/AutoBeInterfaceAuthorizationProgrammer"; import { AutoBeInterfaceOperationProgrammer } from "./programmers/AutoBeInterfaceOperationProgrammer"; import { IAutoBeInterfaceOperationApplication } from "./structures/IAutoBeInterfaceOperationApplication"; import { AutoBeJsonSchemaCollection } from "./utils/AutoBeJsonSchemaCollection"; import { AutoBeJsonSchemaFactory } from "./utils/AutoBeJsonSchemaFactory"; import { AutoBeJsonSchemaNamingConvention } from "./utils/AutoBeJsonSchemaNamingConvention"; export async function orchestrateInterfaceOperation( ctx: AutoBeContext, props: { instruction: string; designs: AutoBeInterfaceEndpointDesign[]; }, ): Promise<AutoBeOpenApi.IOperation[]> { // write const progress: AutoBeProgressEventBase = { total: props.designs.length, completed: 0, }; const written: AutoBeOpenApi.IOperation[] = ( await executeCachedBatch( ctx, props.designs.map((design) => async (promptCacheKey) => { const counter = new Singleton(() => ++progress.completed); try { const row: AutoBeOpenApi.IOperation[] = await forceRetry( () => process(ctx, { counter, design, progress, promptCacheKey, instruction: props.instruction, }), 3, () => true, ); return row; } catch (error) { console.log("operation", design, error); counter.get(); throw error; } }), ) ).flat(); // unique dictionary const unique: HashMap<AutoBeOpenApi.IEndpoint, AutoBeOpenApi.IOperation> = new HashMap( written.map( (w) => new Pair( { path: w.path, method: w.method, }, w, ), ), AutoBeOpenApiEndpointComparator.hashCode, AutoBeOpenApiEndpointComparator.equals, ); // review removed — write agents self-review during rewrite loop const operations: AutoBeOpenApi.IOperation[] = unique .toJSON() .map((it) => it.second); AutoBeJsonSchemaNamingConvention.normalize({ operations, collection: new AutoBeJsonSchemaCollection({}, {}), }); const analyze: AutoBeAnalyzeHistory = ctx.state().analyze!; const sessionTypeNames: string[] = analyze.actors.map((actor) => AutoBeInterfaceAuthorizationProgrammer.getSessionTypeName({ prefix: analyze.prefix, actor: actor.name, }), ); if (sessionTypeNames.length === 0) return operations; return operations.filter((op) => { const predicate = (typeName: string | undefined): boolean => { if (typeName === undefined) return true; return sessionTypeNames.every( (x) => typeName !== `${x}.ICreate` && typeName !== `${x}.IUpdate`, ); }; return ( predicate(op.requestBody?.typeName) && predicate(op.responseBody?.typeName) ); }); } async function process( ctx: AutoBeContext, props: { counter: Singleton<number>; design: AutoBeInterfaceEndpointDesign; progress: AutoBeProgressEventBase; promptCacheKey: string; instruction: string; }, ): Promise<AutoBeOpenApi.IOperation[]> { const allSections: IAnalysisSectionEntry[] = convertToSectionEntries( ctx.state().analyze?.files ?? [], ); const pathSegments = props.design.endpoint.path .split("/") .filter((p) => p && !p.startsWith(":") && !p.startsWith("{")); const queryText: string = [ "operation", props.design.endpoint.method, ...pathSegments, ].join(" "); const ragSections: IAnalysisSectionEntry[] = await buildAnalysisContextSections( getEmbedder(), allSections, queryText, "TOPK", { log: false, logPrefix: "interfaceOperation" }, ); const prefix: string = NamingConvention.camel(ctx.state().analyze!.prefix); const preliminary: AutoBePreliminaryController< | "analysisSections" | "databaseSchemas" | "previousAnalysisSections" | "previousDatabaseSchemas" | "previousInterfaceOperations" | "complete" > = new AutoBePreliminaryController({ dispatch: (e) => ctx.dispatch(e), state: ctx.state(), application: typia.json.application<IAutoBeInterfaceOperationApplication>(), source: SOURCE, kinds: [ "analysisSections", "databaseSchemas", "previousAnalysisSections", "previousDatabaseSchemas", "previousInterfaceOperations", "complete", ], local: { analysisSections: ragSections, }, }); const event: AutoBeInterfaceOperationEvent = await preliminary.orchestrate( ctx, async (out) => { const pointer: IPointer<IAutoBeInterfaceOperationApplication.IWrite | null> = { value: null, }; const result: AutoBeContext.IResult = await ctx.conversate({ source: SOURCE, controller: createController({ preliminary, build: (complete) => { pointer.value = complete; }, }), enforceFunctionCall: true, promptCacheKey: props.promptCacheKey, ...transformInterfaceOperationHistory({ endpoint: props.design.endpoint, instruction: props.instruction, prefix, preliminary, }), }); if (pointer.value === null) return out(result)(null); AutoBeInterfaceOperationProgrammer.fix(pointer.value.operation); for (const p of pointer.value.operation.parameters) p.schema = AutoBeJsonSchemaFactory.fixSchema(p.schema); // Use authorizationActors from endpoint design (not from LLM) const authorizationActors: string[] = props.design.authorizationActors; const matrix: AutoBeOpenApi.IOperation[] = authorizationActors.length === 0 ? [ { ...pointer.value.operation, path: "/" + [prefix, ...pointer.value.operation.path.split("/")] .filter((it) => it !== "") .join("/"), authorizationActor: null, authorizationType: null, prerequisites: [], } satisfies AutoBeOpenApi.IOperation, ] : authorizationActors.map( (actor) => ({ ...pointer.value!.operation, path: "/" + [prefix, actor, ...pointer.value!.operation.path.split("/")] .filter((it) => it !== "") .join("/"), authorizationActor: actor, authorizationType: null, prerequisites: [], }) satisfies AutoBeOpenApi.IOperation, ); props.counter.get(); return out(result)({ type: SOURCE, id: v7(), analysis: pointer.value.analysis, rationale: pointer.value.rationale, operations: matrix, acquisition: preliminary.getAcquisition(), metric: result.metric, tokenUsage: result.tokenUsage, ...props.progress, step: ctx.state().analyze?.step ?? 0, created_at: new Date().toISOString(), } satisfies AutoBeInterfaceOperationEvent); }, ); ctx.dispatch(event); return event.operations; } function createController(props: { preliminary: AutoBePreliminaryController< | "analysisSections" | "databaseSchemas" | "previousAnalysisSections" | "previousDatabaseSchemas" | "previousInterfaceOperations" | "complete" >; build: (operation: IAutoBeInterfaceOperationApplication.IWrite) => void; }): IAgenticaController.IClass { const validate = ( next: unknown, ): IValidation<IAutoBeInterfaceOperationApplication.IProps> => { const result: IValidation<IAutoBeInterfaceOperationApplication.IProps> = typia.validate<IAutoBeInterfaceOperationApplication.IProps>(next); if (result.success === false) return result; else if (result.data.request.type !== "write") return props.preliminary.validate({ thinking: result.data.thinking, request: result.data.request, }); const errors: IValidation.IError[] = []; AutoBeInterfaceOperationProgrammer.validate({ accessor: "$input.request.operation", errors, operation: result.data.request.operation, }); if (errors.length !== 0) return { success: false, errors, data: next, }; return result; }; const application: ILlmApplication = props.preliminary.fixApplication( typia.llm.application<IAutoBeInterfaceOperationApplication>({ validate: { process: validate, }, }), ); return { protocol: "class", name: SOURCE, application, execute: { process: (next) => { if (next.request.type === "write") props.build(next.request); }, } satisfies IAutoBeInterfaceOperationApplication, }; } const SOURCE = "interfaceOperation" satisfies AutoBeEventSource;