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