@autobe/agent
Version:
AI backend server code generator
445 lines (418 loc) • 13.8 kB
text/typescript
import {
AutoBeProgressEventBase,
AutoBeRealizeCorrectEvent,
AutoBeRealizeFunction,
AutoBeRealizeValidateEvent,
IAutoBeTypeScriptCompileResult,
} from "@autobe/interface";
import { IPointer, Singleton } from "tstl";
import typia, { ILlmApplication, ILlmController, IValidation } from "typia";
import { v7 } from "uuid";
import { AutoBeConfigConstant } from "../../../constants/AutoBeConfigConstant";
import { AutoBeContext } from "../../../context/AutoBeContext";
import { executeCachedBatch } from "../../../utils/executeCachedBatch";
import { forceRetry } from "../../../utils/forceRetry";
import { IAutoBeCommonCorrectCastingApplication } from "../../common/structures/IAutoBeCommonCorrectCastingApplication";
import { transformRealizeCorrectCastingHistory } from "../histories/transformRealizeCorrectCastingHistory";
import { compileRealizeFiles } from "../programmers/compileRealizeFiles";
import { IAutoBeRealizeFunctionFailure } from "../structures/IAutoBeRealizeFunctionFailure";
/** Result of attempting to correct a single function */
interface ICorrectionResult<RealizeFunction extends AutoBeRealizeFunction> {
type: "success" | "ignore" | "exception";
function: RealizeFunction;
}
interface IProgrammer<RealizeFunction extends AutoBeRealizeFunction> {
template(func: RealizeFunction): string;
replaceImportStatements(props: {
function: RealizeFunction;
code: string;
}): Promise<string>;
additional(functions: RealizeFunction[]): Record<string, string>;
location: string;
}
export const orchestrateRealizeCorrectCasting = async <
RealizeFunction extends AutoBeRealizeFunction,
>(
ctx: AutoBeContext,
props: {
programmer: IProgrammer<RealizeFunction>;
functions: RealizeFunction[];
progress: AutoBeProgressEventBase;
},
life: number = AutoBeConfigConstant.COMPILER_RETRY,
): Promise<RealizeFunction[]> => {
const validateEvent: AutoBeRealizeValidateEvent = await compileWithFiltering(
ctx,
{
functions: props.functions,
programmer: props.programmer,
progress: props.progress,
},
);
return predicate(
ctx,
{
programmer: props.programmer,
functions: props.functions,
previousFailures: [],
progress: props.progress,
event: validateEvent,
},
life,
);
};
const predicate = async <RealizeFunction extends AutoBeRealizeFunction>(
ctx: AutoBeContext,
props: {
programmer: IProgrammer<RealizeFunction>;
functions: RealizeFunction[];
previousFailures: IAutoBeRealizeFunctionFailure<RealizeFunction>[][];
progress: AutoBeProgressEventBase;
event: AutoBeRealizeValidateEvent;
},
life: number,
): Promise<RealizeFunction[]> => {
if (props.event.result.type === "failure") {
ctx.dispatch(props.event);
return await correct(ctx, props, life);
}
return props.functions;
};
const correct = async <RealizeFunction extends AutoBeRealizeFunction>(
ctx: AutoBeContext,
props: {
programmer: IProgrammer<RealizeFunction>;
functions: RealizeFunction[];
previousFailures: IAutoBeRealizeFunctionFailure<RealizeFunction>[][];
progress: AutoBeProgressEventBase;
event: AutoBeRealizeValidateEvent;
},
life: number,
): Promise<RealizeFunction[]> => {
// Early returns for non-correctable cases
if (props.event.result.type !== "failure" || life < 0) {
return props.functions;
}
const failure: IAutoBeTypeScriptCompileResult.IFailure = props.event.result;
const errorLocations: string[] = getErrorFiles({
location: props.programmer.location,
failure,
}).filter((l) => props.functions.map((f) => f.location).includes(l));
// If no locations to correct, return original functions
if (errorLocations.length === 0) {
return props.functions;
}
props.progress.total += errorLocations.length;
const converted: ICorrectionResult<RealizeFunction>[] =
await executeCachedBatch(
ctx,
errorLocations.map(
(location) => async (): Promise<ICorrectionResult<RealizeFunction>> => {
const counter = new Singleton(() => ++props.progress.completed);
const localFunction: RealizeFunction = props.functions.find(
(f) => f.location === location,
)!;
const localPreviousFailures: IAutoBeRealizeFunctionFailure<RealizeFunction>[] =
props.previousFailures
.map(
(pf) =>
pf.find(
(f) => f.function.location === localFunction.location,
) ?? null,
)
.filter((x) => x !== null);
const localDiagnostics: IAutoBeTypeScriptCompileResult.IDiagnostic[] =
failure.diagnostics.filter(
(d) => d.file === localFunction.location,
);
try {
return await forceRetry(() =>
process(ctx, {
programmer: props.programmer,
function: localFunction,
previousFailures: localPreviousFailures,
diagnostic: localDiagnostics,
progress: props.progress,
}),
);
} catch (error) {
console.log("realizeCorrectCasting", localFunction.location, error);
counter.get();
return {
type: "exception",
function: localFunction,
};
}
},
),
);
// Get functions that were not modified (not in locations array)
const unchangedFunctions: RealizeFunction[] = props.functions.filter(
(f) => !errorLocations.includes(f.location),
);
// Merge converted functions with unchanged functions for validation
const allFunctionsForValidation = [
...converted.map((c) => c.function),
...unchangedFunctions,
];
const newValidate: AutoBeRealizeValidateEvent = await compileWithFiltering(
ctx,
{
functions: allFunctionsForValidation,
programmer: props.programmer,
progress: props.progress,
},
);
const newResult: IAutoBeTypeScriptCompileResult = newValidate.result;
if (newResult.type === "success") {
return allFunctionsForValidation;
} else if (newResult.type === "exception") {
// Compilation exception, return current functions. because retrying won't help.
return props.functions;
}
const newLocations: string[] =
newValidate.result.type === "failure"
? getErrorFiles({
failure: newValidate.result,
location: props.programmer.location,
})
: [];
// Separate successful, failed, and ignored corrections
const { success, failed, ignored } = separateCorrectionResults(
converted,
newLocations,
);
// If no failures to retry, return all functions
if (failed.length === 0) {
return [...success, ...ignored, ...unchangedFunctions];
}
// Recursively retry failed functions
const retriedFunctions: RealizeFunction[] = await predicate(
ctx,
{
programmer: props.programmer,
functions: failed,
previousFailures: [
...props.previousFailures,
failed.map(
(f) =>
({
function: f,
diagnostics:
newValidate.result.type === "failure"
? newValidate.result.diagnostics.filter(
(d) => d.file === f.location,
)
: [],
}) satisfies IAutoBeRealizeFunctionFailure<RealizeFunction>,
),
],
progress: props.progress,
event: newValidate,
},
life - 1,
);
return [...success, ...ignored, ...retriedFunctions, ...unchangedFunctions];
};
const process = async <RealizeFunction extends AutoBeRealizeFunction>(
ctx: AutoBeContext,
props: {
programmer: IProgrammer<RealizeFunction>;
function: RealizeFunction;
previousFailures: IAutoBeRealizeFunctionFailure<RealizeFunction>[];
diagnostic: IAutoBeTypeScriptCompileResult.IDiagnostic[];
progress: AutoBeProgressEventBase;
},
): Promise<ICorrectionResult<RealizeFunction>> => {
const template: string = props.programmer.template(props.function);
const pointer: IPointer<
IAutoBeCommonCorrectCastingApplication.IProps | false | null
> = {
value: null,
};
const { metric, tokenUsage } = await ctx.conversate({
source: "realizeCorrect",
controller: createController({
then: (next) => {
pointer.value = next;
},
reject: () => {
pointer.value = false;
},
}),
enforceFunctionCall: true,
...transformRealizeCorrectCastingHistory({
template,
function: props.function,
failures: [
...props.previousFailures,
{
function: props.function,
diagnostics: props.diagnostic,
},
],
}),
});
if (pointer.value === null)
return {
type: "exception",
function: props.function,
};
else if (pointer.value === false)
return {
type: "ignore",
function: props.function,
};
const content: string = await props.programmer.replaceImportStatements({
function: props.function,
code: pointer.value.revise.final ?? pointer.value.draft,
});
const corrected: RealizeFunction = {
...props.function,
content,
template,
};
ctx.dispatch({
id: v7(),
type: "realizeCorrect",
kind: "casting",
function: corrected,
created_at: new Date().toISOString(),
step: ctx.state().analyze?.step ?? 0,
metric,
tokenUsage,
} satisfies AutoBeRealizeCorrectEvent);
return {
type: "success",
function: corrected,
};
};
const createController = (props: {
then: (next: IAutoBeCommonCorrectCastingApplication.IProps) => void;
reject: () => void;
}): ILlmController => {
const validate = (
input: unknown,
): IValidation<IAutoBeCommonCorrectCastingApplication.IProps> => {
const result: IValidation<IAutoBeCommonCorrectCastingApplication.IProps> =
typia.validate<IAutoBeCommonCorrectCastingApplication.IProps>(input);
if (result.success === false) return result;
// @todo: validate empty code?
return result;
};
const application: ILlmApplication =
typia.llm.application<IAutoBeCommonCorrectCastingApplication>({
validate: {
rewrite: validate,
reject: () => ({
success: true,
data: undefined,
}),
},
});
return {
protocol: "class",
name: "correctInvalidRequest",
application,
execute: {
rewrite: (next) => {
props.then(next);
},
reject: () => {
props.reject();
},
} satisfies IAutoBeCommonCorrectCastingApplication,
};
};
/**
* Extract unique file locations from validation event diagnostics
*
* @param event - Validation event containing compilation results
* @returns Array of unique file paths that have errors
*/
const getErrorFiles = (props: {
failure: IAutoBeTypeScriptCompileResult.IFailure;
location: string;
}): string[] => {
const diagnostics: IAutoBeTypeScriptCompileResult.IDiagnostic[] =
props.failure.diagnostics;
const locations: string[] = diagnostics
.map((d) => d.file)
.filter((f): f is string => f !== null)
.filter((f) => f.startsWith(props.location));
return Array.from(new Set(locations));
};
const compileWithFiltering = async <
RealizeFunction extends AutoBeRealizeFunction,
>(
ctx: AutoBeContext,
props: {
functions: RealizeFunction[];
programmer: IProgrammer<RealizeFunction>;
progress: AutoBeProgressEventBase;
},
): Promise<AutoBeRealizeValidateEvent> => {
const compiled: AutoBeRealizeValidateEvent = await compileRealizeFiles(ctx, {
functions: props.functions,
additional: props.programmer.additional(props.functions),
progress: (result) => {
if (result.type === "success")
props.progress.completed = props.functions.length;
else if (result.type === "failure")
props.progress.completed =
props.progress.total -
new Set(
result.diagnostics
.map((d) => d.file)
.filter((f) => f !== null)
.filter((x) => !!props.functions.find((y) => y.location === x)),
).size;
return props.progress;
},
});
if (compiled.result.type !== "failure") {
return compiled;
}
const functionLocations: string[] = props.functions.map((f) => f.location);
compiled.result.diagnostics = compiled.result.diagnostics.filter(
(d) => d.file !== null && functionLocations.includes(d.file),
);
if (compiled.result.diagnostics.length === 0) {
compiled.result = { type: "success" };
}
return compiled;
};
/**
* Separate correction results into successful, failed, and ignored functions
*
* @param corrections - Array of correction results
* @param errorLocations - File paths that still have errors
* @returns Object with success, failed, and ignored function arrays
*/
const separateCorrectionResults = <
RealizeFunction extends AutoBeRealizeFunction,
>(
corrections: ICorrectionResult<RealizeFunction>[],
errorLocations: string[],
): {
success: RealizeFunction[];
failed: RealizeFunction[];
ignored: RealizeFunction[];
} => {
const success: RealizeFunction[] = corrections
.filter(
(c) =>
c.type === "success" && !errorLocations.includes(c.function.location),
)
.map((c) => c.function);
const failed: RealizeFunction[] = corrections
.filter(
(c) =>
c.type === "success" && errorLocations.includes(c.function.location),
)
.map((c) => c.function);
const ignored: RealizeFunction[] = corrections
.filter((c) => c.type === "ignore" || c.type === "exception")
.map((c) => c.function);
return { success, failed, ignored };
};