@autobe/agent
Version:
AI backend server code generator
226 lines (212 loc) • 7 kB
text/typescript
import { IAgenticaController } from "@agentica/core";
import {
AutoBeDatabaseComponent,
AutoBeDatabaseComponentEvent,
AutoBeDatabaseGroup,
AutoBeEventSource,
AutoBeProgressEventBase,
} from "@autobe/interface";
import { IPointer, 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 { getEmbedder } from "../../utils/getEmbedder";
import { AutoBePreliminaryController } from "../common/AutoBePreliminaryController";
import { convertToSectionEntries } from "../common/internal/convertToSectionEntries";
import { IAnalysisSectionEntry } from "../common/structures/IAnalysisSectionEntry";
import { transformDatabaseComponentsHistory } from "./histories/transformDatabaseComponentsHistory";
import { AutoBeDatabaseComponentProgrammer } from "./programmers/AutoBeDatabaseComponentProgrammer";
import { IAutoBeDatabaseComponentApplication } from "./structures/IAutoBeDatabaseComponentApplication";
export async function orchestrateDatabaseComponent(
ctx: AutoBeContext,
props: {
instruction: string;
groups: AutoBeDatabaseGroup[];
},
): Promise<AutoBeDatabaseComponent[]> {
// Filter to only process domain groups - authorization groups are handled
// by orchestratePrismaAuthorization separately
const domainGroups = props.groups.filter((g) => g.kind === "domain");
if (domainGroups.length === 0) return [];
const prefix: string | null = ctx.state().analyze?.prefix ?? null;
const progress: AutoBeProgressEventBase = {
completed: 0,
total: domainGroups.length,
};
const components: AutoBeDatabaseComponent[] = await executeCachedBatch(
ctx,
domainGroups.map((group) => async (promptCacheKey) => {
const counter = new Singleton(() => ++progress.completed);
try {
const component: AutoBeDatabaseComponent = await process(ctx, {
group,
instruction: props.instruction,
prefix,
progress,
counter,
promptCacheKey,
});
return component;
} catch (error) {
counter.get();
throw error;
}
}),
);
return AutoBeDatabaseComponentProgrammer.removeDuplicatedTable(components);
}
async function process(
ctx: AutoBeContext,
props: {
group: AutoBeDatabaseGroup;
instruction: string;
prefix: string | null;
progress: AutoBeProgressEventBase;
counter: Singleton<number>;
promptCacheKey: string;
},
): Promise<AutoBeDatabaseComponent> {
const allSections: IAnalysisSectionEntry[] = convertToSectionEntries(
ctx.state().analyze?.files ?? [],
);
const queryText: string = [
"prisma",
"schema",
"component",
props.group.filename,
props.group.namespace,
].join(" ");
const ragSections: IAnalysisSectionEntry[] =
await buildAnalysisContextSections(
getEmbedder(),
allSections,
queryText,
"TOPK",
{ log: false, logPrefix: "prismaComponent" },
);
const preliminary: AutoBePreliminaryController<
| "analysisSections"
| "previousAnalysisSections"
| "previousDatabaseSchemas"
| "complete"
> = new AutoBePreliminaryController({
dispatch: (e) => ctx.dispatch(e),
application: typia.json.application<IAutoBeDatabaseComponentApplication>(),
source: SOURCE,
kinds: [
"analysisSections",
"previousAnalysisSections",
"previousDatabaseSchemas",
"complete",
],
state: ctx.state(),
local: {
analysisSections: ragSections,
},
});
const event: AutoBeDatabaseComponentEvent = await preliminary.orchestrate(
ctx,
async (out) => {
const pointer: IPointer<IAutoBeDatabaseComponentApplication.IWrite | null> =
{
value: null,
};
const result: AutoBeContext.IResult = await ctx.conversate({
source: SOURCE,
controller: createController({
pointer,
preliminary,
prefix: props.prefix,
}),
enforceFunctionCall: true,
promptCacheKey: props.promptCacheKey,
...transformDatabaseComponentsHistory(ctx.state(), {
instruction: props.instruction,
prefix: props.prefix,
preliminary,
group: props.group,
}),
});
if (pointer.value === null) return out(result)(null);
// Build complete component from group skeleton + tables
const component: AutoBeDatabaseComponent = {
...props.group,
tables: pointer.value.tables,
};
return out(result)({
type: SOURCE,
id: v7(),
created_at: new Date().toISOString(),
analysis: pointer.value.analysis,
rationale: pointer.value.rationale,
component,
acquisition: preliminary.getAcquisition(),
metric: result.metric,
tokenUsage: result.tokenUsage,
step: ctx.state().analyze?.step ?? 0,
total: props.progress.total,
completed: props.counter.get(),
});
},
);
ctx.dispatch(event);
return event.component;
}
function createController(props: {
pointer: IPointer<IAutoBeDatabaseComponentApplication.IWrite | null>;
preliminary: AutoBePreliminaryController<
| "analysisSections"
| "previousAnalysisSections"
| "previousDatabaseSchemas"
| "complete"
>;
prefix: string | null;
}): IAgenticaController.IClass {
const validate: Validator = (input) => {
const result: IValidation<IAutoBeDatabaseComponentApplication.IProps> =
typia.validate<IAutoBeDatabaseComponentApplication.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[] = [];
AutoBeDatabaseComponentProgrammer.validate({
errors,
prefix: props.prefix,
tables: result.data.request.tables,
path: "$input.request.tables",
});
if (errors.length > 0)
return {
success: false,
data: result.data,
errors,
};
return result;
};
const application: ILlmApplication = props.preliminary.fixApplication(
typia.llm.application<IAutoBeDatabaseComponentApplication>({
validate: {
process: validate,
},
}),
);
return {
protocol: "class",
name: SOURCE,
application,
execute: {
process: (input) => {
if (input.request.type === "write") props.pointer.value = input.request;
},
} satisfies IAutoBeDatabaseComponentApplication,
};
}
type Validator = (
input: unknown,
) => IValidation<IAutoBeDatabaseComponentApplication.IProps>;
const SOURCE = "databaseComponent" satisfies AutoBeEventSource;