UNPKG

@autobe/agent

Version:

AI backend server code generator

295 lines (277 loc) 9.33 kB
import { IAgenticaController } from "@agentica/core"; import { AutoBeEventSource, AutoBeInterfaceAuthorization, AutoBeOpenApi, AutoBeProgressEventBase, AutoBeTestScenario, AutoBeTestScenarioEvent, } from "@autobe/interface"; import { AutoBeOpenApiEndpointComparator } from "@autobe/utils"; import { NamingConvention } from "@typia/utils"; import { HashMap, HashSet, 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 { transformTestScenarioHistory } from "./histories/transformTestScenarioHistory"; import { AutoBeTestScenarioProgrammer } from "./programmers/AutoBeTestScenarioProgrammer"; import { IAutoBeTestScenarioApplication } from "./structures/IAutoBeTestScenarioApplication"; import { getPrerequisites } from "./utils/getPrerequisites"; /** * Orchestrate test scenario generation for all API operations. * * Following the InterfacePrerequisite pattern: * * - Generate one scenario per operation in parallel * - Review all generated scenarios in parallel * - Return final scenarios array * * @param ctx - AutoBe context * @param instruction - E2E-test-specific instructions from requirements * @returns Array of reviewed test scenarios */ export const orchestrateTestScenario = async ( ctx: AutoBeContext, instruction: string, ): Promise<AutoBeTestScenario[]> => { const document: AutoBeOpenApi.IDocument | undefined = ctx.state().interface?.document; if (document === undefined) { throw new Error( "Cannot write test scenarios because there are no operations.", ); } const dict: HashMap<AutoBeOpenApi.IEndpoint, AutoBeOpenApi.IOperation> = AutoBeTestScenarioProgrammer.associate(document.operations); const progress: AutoBeProgressEventBase = { total: document.operations.length, completed: 0, }; const matrix: AutoBeTestScenario[][] = await executeCachedBatch( ctx, document.operations.map((operation) => async (promptCacheKey) => { const counter = new Singleton(() => ++progress.completed); try { return await process(ctx, { dict, document, operation, progress, counter, promptCacheKey, instruction, }); } catch (error) { counter.get(); console.log(operation, error); return []; } }), ); const scenarios: AutoBeTestScenario[] = matrix.flat(); // review removed — write agents self-review during rewrite loop return scenarios; }; /** * Process single operation scenario generation. * * Following InterfacePrerequisite pattern: * * - Preliminary.orchestrate wrapper * - Conversate with controller * - Dispatch event * - Return scenario */ async function process( ctx: AutoBeContext, props: { dict: HashMap<AutoBeOpenApi.IEndpoint, AutoBeOpenApi.IOperation>; operation: AutoBeOpenApi.IOperation; document: AutoBeOpenApi.IDocument; progress: AutoBeProgressEventBase; counter: Singleton<number>; promptCacheKey: string; instruction: string; }, ): Promise<AutoBeTestScenario[]> { const allSections: IAnalysisSectionEntry[] = convertToSectionEntries( ctx.state().analyze?.files ?? [], ); const pathSegments = props.operation.path .split("/") .filter((p) => p && !p.startsWith(":") && !p.startsWith("{")); const queryText: string = [ "test", "scenario", props.operation.method, ...pathSegments, ].join(" "); const ragSections: IAnalysisSectionEntry[] = await buildAnalysisContextSections( getEmbedder(), allSections, queryText, "TOPK", { log: false, logPrefix: "testScenario" }, ); const authorizations: AutoBeInterfaceAuthorization[] = ctx.state().interface?.authorizations ?? []; const preliminary: AutoBePreliminaryController< "analysisSections" | "interfaceOperations" | "interfaceSchemas" | "complete" > = new AutoBePreliminaryController({ application: typia.json.application<IAutoBeTestScenarioApplication>(), source: SOURCE, kinds: [ "analysisSections", "interfaceOperations", "interfaceSchemas", "complete", ], dispatch: (e) => ctx.dispatch(e), state: ctx.state(), all: { interfaceOperations: props.document.operations, }, local: { analysisSections: ragSections, interfaceOperations: (() => { const unique: HashSet<AutoBeOpenApi.IEndpoint> = new HashSet( AutoBeOpenApiEndpointComparator.hashCode, AutoBeOpenApiEndpointComparator.equals, ); unique.insert({ method: props.operation.method, path: props.operation.path, }); for (const pr of getPrerequisites({ document: props.document, endpoint: props.operation, })) unique.insert(pr.endpoint); return unique.toJSON().map((endpoint) => props.dict.get(endpoint)); })(), }, }); const event: AutoBeTestScenarioEvent = await preliminary.orchestrate( ctx, async (out) => { const pointer: IPointer<AutoBeTestScenario[] | null> = { value: null, }; const result: AutoBeContext.IResult = await ctx.conversate({ source: SOURCE, controller: createController({ dict: props.dict, operation: props.operation, authorizations, preliminary, build: (scenarios) => { // Normalize function name to snake_case for (const s of scenarios) s.functionName = NamingConvention.snake(s.functionName); pointer.value ??= []; pointer.value.push(...scenarios); }, }), enforceFunctionCall: true, promptCacheKey: props.promptCacheKey, ...transformTestScenarioHistory({ state: ctx.state(), operation: props.operation, instruction: props.instruction, preliminary, }), }); if (pointer.value === null) return out(result)(null); pointer.value.splice(3); return out(result)({ type: SOURCE, id: v7(), metric: result.metric, tokenUsage: result.tokenUsage, scenarios: pointer.value, acquisition: preliminary.getAcquisition(), total: props.progress.total, completed: props.counter.get(), step: ctx.state().interface?.step ?? 0, created_at: new Date().toISOString(), }); }, ); ctx.dispatch(event); return event.scenarios; } function createController(props: { dict: HashMap<AutoBeOpenApi.IEndpoint, AutoBeOpenApi.IOperation>; authorizations: AutoBeInterfaceAuthorization[]; operation: AutoBeOpenApi.IOperation; build: (scenarios: AutoBeTestScenario[]) => void; preliminary: AutoBePreliminaryController< "analysisSections" | "interfaceOperations" | "interfaceSchemas" | "complete" >; }): IAgenticaController.IClass { const validate = ( next: unknown, ): IValidation<IAutoBeTestScenarioApplication.IProps> => { const result: IValidation<IAutoBeTestScenarioApplication.IProps> = typia.validate<IAutoBeTestScenarioApplication.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[] = []; result.data.request.scenarios.forEach((scenario, i) => AutoBeTestScenarioProgrammer.validate({ errors, dict: props.dict, operation: props.operation, scenario, accessor: `$input.request.scenarios[${i}]`, }), ); return errors.length === 0 ? result : { success: false, data: result.data, errors, }; }; const application: ILlmApplication = props.preliminary.fixApplication( typia.llm.application<IAutoBeTestScenarioApplication>({ validate: { process: validate, }, }), ); return { protocol: "class", name: SOURCE, application, execute: { process: (next) => { if (next.request.type === "write") { // Fulfill missing authentication dependencies for each scenario for (const scenario of next.request.scenarios) { AutoBeTestScenarioProgrammer.fulfill({ dict: props.dict, authorizations: props.authorizations, operation: props.operation, scenario, }); } props.build(next.request.scenarios); } }, } satisfies IAutoBeTestScenarioApplication, }; } const SOURCE = "testScenario" satisfies AutoBeEventSource;