@autobe/agent
Version:
AI backend server code generator
280 lines (262 loc) • 8.85 kB
text/typescript
import {
AutoBeEventSource,
AutoBeInterfaceHistory,
AutoBeOpenApi,
AutoBeProgressEventBase,
AutoBeRealizeCollectorPlan,
AutoBeRealizePlanEvent,
} from "@autobe/interface";
import { StringUtil } from "@autobe/utils";
import { IPointer, Singleton } from "tstl";
import typia, { ILlmApplication, ILlmController, IValidation } from "typia";
import { v4 } 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 { transformRealizeCollectorPlanHistory } from "./histories/transformRealizeCollectorPlanHistory";
import { AutoBeRealizeCollectorProgrammer } from "./programmers/AutoBeRealizeCollectorProgrammer";
import { IAutoBeRealizeCollectorPlanApplication } from "./structures/IAutoBeRealizeCollectorPlanApplication";
export async function orchestrateRealizeCollectorPlan(
ctx: AutoBeContext,
props: {
progress: AutoBeProgressEventBase;
},
): Promise<AutoBeRealizeCollectorPlan[]> {
const history: AutoBeInterfaceHistory | null = ctx.state().interface;
if (history === null)
throw new Error("Cannot realize collector plan without interface.");
const document: AutoBeOpenApi.IDocument = history.document;
const dtoTypeNames: string[] = Object.keys(
document.components.schemas,
).filter(AutoBeRealizeCollectorProgrammer.filter);
const prismaSchemaNames: Set<string> = new Set(
ctx
.state()
.database!.result.data.files.map((f) => f.models)
.flat()
.map((m) => m.name),
);
const result: AutoBeRealizeCollectorPlan[][] = await executeCachedBatch(
ctx,
Array.from(dtoTypeNames).map((it) => async (promptCacheKey) => {
const counter = new Singleton(() => ++props.progress.completed);
try {
return await forceRetry(() =>
process(ctx, {
document,
dtoTypeName: it,
prismaSchemaNames,
promptCacheKey,
progress: props.progress,
counter,
}),
);
} catch (error) {
counter.get();
throw error;
}
}),
);
return result.flat();
}
async function process(
ctx: AutoBeContext,
props: {
document: AutoBeOpenApi.IDocument;
dtoTypeName: string;
prismaSchemaNames: Set<string>;
promptCacheKey: string;
progress: AutoBeProgressEventBase;
counter: Singleton<number>;
},
): Promise<AutoBeRealizeCollectorPlan[]> {
const allSections: IAnalysisSectionEntry[] = convertToSectionEntries(
ctx.state().analyze?.files ?? [],
);
const queryText: string = [
"collector",
"plan",
"dto",
"prisma",
props.dtoTypeName,
].join(" ");
const ragSections: IAnalysisSectionEntry[] =
await buildAnalysisContextSections(
getEmbedder(),
allSections,
queryText,
"TOPK",
{ log: false, logPrefix: "realizeCollectorPlan" },
);
const preliminary: AutoBePreliminaryController<
| "analysisSections"
| "databaseSchemas"
| "interfaceSchemas"
| "interfaceOperations"
| "complete"
> = new AutoBePreliminaryController({
dispatch: (e) => ctx.dispatch(e),
state: ctx.state(),
source: SOURCE,
application:
typia.json.application<IAutoBeRealizeCollectorPlanApplication>(),
kinds: [
"analysisSections",
"databaseSchemas",
"interfaceSchemas",
"interfaceOperations",
"complete",
],
local: {
analysisSections: ragSections,
interfaceOperations: props.document.operations.filter(
(op) => op.requestBody?.typeName === props.dtoTypeName,
),
interfaceSchemas: Object.fromEntries(
Object.entries(props.document.components.schemas).filter(
([key]) => key === props.dtoTypeName,
),
),
},
});
const event: AutoBeRealizePlanEvent = await preliminary.orchestrate(
ctx,
async (out) => {
const pointer: IPointer<IAutoBeRealizeCollectorPlanApplication.IWrite | null> =
{
value: null,
};
const result: AutoBeContext.IResult = await ctx.conversate({
source: "realizePlan",
controller: createController({
prismaSchemaNames: props.prismaSchemaNames,
dtoTypeName: props.dtoTypeName,
build: (next) => {
pointer.value = next;
},
preliminary,
}),
enforceFunctionCall: true,
promptCacheKey: props.promptCacheKey,
...transformRealizeCollectorPlanHistory({
state: ctx.state(),
preliminary,
dtoTypeName: props.dtoTypeName,
}),
});
if (pointer.value === null) return out(result)(null);
const plans: AutoBeRealizeCollectorPlan[] = pointer.value.plans
.filter((p) => p.databaseSchemaName !== null)
.map((p) => ({
type: "collector",
dtoTypeName: p.dtoTypeName,
thinking: p.thinking,
databaseSchemaName: p.databaseSchemaName!,
references: p.references,
}));
const event: AutoBeRealizePlanEvent = {
type: "realizePlan",
id: v4(),
plans,
acquisition: preliminary.getAcquisition(),
metric: result.metric,
tokenUsage: result.tokenUsage,
completed: props.counter.get(),
total: props.progress.total,
step: ctx.state().analyze?.step ?? 0,
created_at: new Date().toISOString(),
};
return out(result)(event);
},
);
ctx.dispatch(event);
return event.plans as AutoBeRealizeCollectorPlan[];
}
function createController(props: {
prismaSchemaNames: Set<string>;
dtoTypeName: string;
build: (next: IAutoBeRealizeCollectorPlanApplication.IWrite) => void;
preliminary: AutoBePreliminaryController<
| "analysisSections"
| "databaseSchemas"
| "interfaceSchemas"
| "interfaceOperations"
| "complete"
>;
}): ILlmController {
const validate: Validator = (input) => {
const result: IValidation<IAutoBeRealizeCollectorPlanApplication.IProps> =
typia.validate<IAutoBeRealizeCollectorPlanApplication.IProps>(input);
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[] = [];
result.data.request.plans.map((plan, i) => {
if (props.dtoTypeName !== plan.dtoTypeName)
errors.push({
path: `$input.request.plans[${i}].dtoTypeName`,
value: plan.dtoTypeName,
expected: JSON.stringify(props.dtoTypeName),
description: StringUtil.trim`
The DTO type name must be ${JSON.stringify(props.dtoTypeName)}.
If you have planned other DTO type's collector,
please entirely remake the plan with ONLY the DTO type
${JSON.stringify(props.dtoTypeName)}.
`,
});
if (
plan.databaseSchemaName !== null &&
props.prismaSchemaNames.has(plan.databaseSchemaName) === false
)
errors.push({
path: `$input.request.plans[${i}].databaseSchemaName`,
value: plan.databaseSchemaName,
expected: Array.from(props.prismaSchemaNames)
.map((s) => JSON.stringify(s))
.join(" | "),
description: StringUtil.trim`
The database schema name must be one of the available database schemas.
${Array.from(props.prismaSchemaNames)
.map((s) => `- ${s}`)
.join("\n")}
`,
});
});
return errors.length
? {
success: false,
errors,
data: result.data,
}
: result;
};
const application: ILlmApplication = props.preliminary.fixApplication(
typia.llm.application<IAutoBeRealizeCollectorPlanApplication>({
validate: {
process: validate,
},
}),
);
return {
protocol: "class",
name: SOURCE,
application,
execute: {
process: (next) => {
if (next.request.type === "write") props.build(next.request);
},
} satisfies IAutoBeRealizeCollectorPlanApplication,
};
}
type Validator = (
input: unknown,
) => IValidation<IAutoBeRealizeCollectorPlanApplication.IProps>;
const SOURCE = "realizePlan" satisfies AutoBeEventSource;