@autobe/agent
Version:
AI backend server code generator
226 lines (216 loc) • 7.75 kB
text/typescript
import { IAgenticaController } from "@agentica/core";
import {
AutoBeOpenApi,
AutoBeProgressEventBase,
AutoBeTestAuthorizeFunction,
AutoBeTestGenerateFunction,
AutoBeTestPrepareFunction,
AutoBeTestScenario,
AutoBeTestWriteEvent,
} from "@autobe/interface";
import { NamingConvention } from "@typia/utils";
import { IPointer, Singleton } from "tstl";
import typia, { ILlmApplication, IValidation } from "typia";
import { v7 } from "uuid";
import { AutoBeContext } from "../../context/AutoBeContext";
import { executeCachedBatch } from "../../utils/executeCachedBatch";
import { forceRetry } from "../../utils/forceRetry";
import { validateEmptyCode } from "../../utils/validateEmptyCode";
import { getTestScenarioArtifacts } from "./compile/getTestArtifacts";
import { transformTestOperationWriteHistory } from "./histories/transformTestOperationWriteHistory";
import { AutoBeTestOperationProgrammer } from "./programmers/AutoBeTestOperationProgrammer";
import { IAutoBeTestOperationProcedure } from "./structures/IAutoBeTestOperationProcedure";
import { IAutoBeTestOperationWriteApplication } from "./structures/IAutoBeTestOperationWriteApplication";
import { IAutoBeTestScenarioArtifacts } from "./structures/IAutoBeTestScenarioArtifacts";
export async function orchestrateTestOperationWrite(
ctx: AutoBeContext,
props: {
instruction: string;
document: AutoBeOpenApi.IDocument;
scenarios: AutoBeTestScenario[];
authorizes: AutoBeTestAuthorizeFunction[];
prepares: AutoBeTestPrepareFunction[];
generates: AutoBeTestGenerateFunction[];
progress: AutoBeProgressEventBase;
},
): Promise<IAutoBeTestOperationProcedure[]> {
const result: Array<IAutoBeTestOperationProcedure | null> =
await executeCachedBatch(
ctx,
/**
* Generate test code for each scenario. Maps through plans array to
* create individual test code implementations. Each scenario is processed
* to generate corresponding test code and progress events.
*/
props.scenarios.map((scenario) => async (promptCacheKey) => {
const artifacts: IAutoBeTestScenarioArtifacts =
await getTestScenarioArtifacts(ctx, scenario);
const usedActors: Set<string> = new Set(
artifacts.document.operations
.map((o) => o.authorizationActor)
.filter((a) => a !== null),
);
const authorizationFunctions: AutoBeTestAuthorizeFunction[] =
props.authorizes.filter((f) => usedActors.has(f.actor));
const generationFunctions: AutoBeTestGenerateFunction[] =
props.generates.filter((f) =>
artifacts.document.operations.some(
(o) =>
o.method === f.endpoint.method && o.path === f.endpoint.path,
),
);
const prepareFunctions: AutoBeTestPrepareFunction[] =
props.prepares.filter((f) =>
Object.keys(artifacts.document.components.schemas).includes(
f.typeName,
),
);
const counter = new Singleton(() => ++props.progress.completed);
try {
return await forceRetry(async () => {
const event: AutoBeTestWriteEvent = await process(ctx, {
document: props.document,
scenario,
authorizes: authorizationFunctions,
generates: generationFunctions,
prepares: prepareFunctions,
artifacts,
progress: props.progress,
counter,
promptCacheKey,
instruction: props.instruction,
});
ctx.dispatch(event);
if (event.function.type !== "operation")
throw new Error(
`Unexpected testOperationWrite function kind: ${event.function.type}`,
);
return {
type: "operation",
artifacts,
function: event.function,
authorizes: authorizationFunctions,
generates: generationFunctions,
prepares: prepareFunctions,
} satisfies IAutoBeTestOperationProcedure;
});
} catch {
counter.get();
return null;
}
}),
);
return result.filter((r) => r !== null);
}
async function process(
ctx: AutoBeContext,
props: {
document: AutoBeOpenApi.IDocument;
authorizes: AutoBeTestAuthorizeFunction[];
generates: AutoBeTestGenerateFunction[];
prepares: AutoBeTestPrepareFunction[];
scenario: AutoBeTestScenario;
artifacts: IAutoBeTestScenarioArtifacts;
progress: AutoBeProgressEventBase;
counter: Singleton<number>;
promptCacheKey: string;
instruction: string;
},
): Promise<AutoBeTestWriteEvent> {
const pointer: IPointer<IAutoBeTestOperationWriteApplication.IProps | null> =
{
value: null,
};
const { metric, tokenUsage } = await ctx.conversate({
source: "testWrite",
controller: createController({
functionName: props.scenario.functionName,
build: (next) => {
next.domain = NamingConvention.snake(next.domain);
pointer.value = next;
},
}),
enforceFunctionCall: true,
promptCacheKey: props.promptCacheKey,
...(await transformTestOperationWriteHistory(ctx, {
authorizationFunctions: props.authorizes,
generationFunctions: props.generates,
scenario: props.scenario,
artifacts: props.artifacts,
instruction: props.instruction,
})),
});
if (pointer.value === null) {
props.counter.get();
throw new Error("Failed to create test code.");
}
const location: string = `test/features/api/${pointer.value.domain}/${props.scenario.functionName}.ts`;
return {
type: "testWrite",
id: v7(),
created_at: new Date().toISOString(),
function: {
type: "operation",
domain: pointer.value.domain,
scenario: props.scenario,
name: props.scenario.functionName,
location,
content: await AutoBeTestOperationProgrammer.replaceImportStatements({
compiler: await ctx.compiler(),
artifacts: props.artifacts,
authorizes: props.authorizes,
prepares: props.prepares,
generates: props.generates,
location,
content: pointer.value.revise.final ?? pointer.value.draft,
}),
},
metric,
tokenUsage,
completed: props.counter.get(),
total: props.progress.total,
step: ctx.state().interface?.step ?? 0,
} satisfies AutoBeTestWriteEvent;
}
function createController(props: {
functionName: string;
build: (next: IAutoBeTestOperationWriteApplication.IProps) => void;
}): IAgenticaController.IClass {
const validate = (
input: unknown,
): IValidation<IAutoBeTestOperationWriteApplication.IProps> => {
const result: IValidation<IAutoBeTestOperationWriteApplication.IProps> =
typia.validate<IAutoBeTestOperationWriteApplication.IProps>(input);
if (result.success === false) return result;
const errors: IValidation.IError[] = validateEmptyCode({
name: props.functionName,
draft: result.data.draft,
revise: result.data.revise,
asynchronous: true,
path: "$input",
});
return errors.length
? {
success: false,
errors,
data: result.data,
}
: result;
};
const application: ILlmApplication =
typia.llm.application<IAutoBeTestOperationWriteApplication>({
validate: {
write: validate,
},
});
return {
protocol: "class",
name: "Create Test Code",
application,
execute: {
write: (next) => {
props.build(next);
},
} satisfies IAutoBeTestOperationWriteApplication,
};
}