@autobe/agent
Version:
AI backend server code generator
585 lines (566 loc) • 21 kB
text/typescript
import {
AgenticaJsonParseError,
AgenticaValidationError,
IMicroAgenticaConfig,
MicroAgentica,
MicroAgenticaHistory,
} from "@agentica/core";
import {
AutoBeAnalyzeCompleteEvent,
AutoBeAnalyzeHistory,
AutoBeAnalyzeStartEvent,
AutoBeAssistantMessageEvent,
AutoBeDatabaseCompleteEvent,
AutoBeDatabaseHistory,
AutoBeDatabaseStartEvent,
AutoBeEvent,
AutoBeFunctionCallingMetric,
AutoBeHistory,
AutoBeInterfaceCompleteEvent,
AutoBeInterfaceHistory,
AutoBeInterfaceStartEvent,
AutoBePhase,
AutoBeProcessAggregate,
AutoBeProcessAggregateCollection,
AutoBeRealizeCompleteEvent,
AutoBeRealizeHistory,
AutoBeRealizeStartEvent,
AutoBeTestCompleteEvent,
AutoBeTestHistory,
AutoBeTestStartEvent,
IAutoBeCompiler,
IAutoBeCompilerListener,
IAutoBeGetFilesOptions,
IAutoBeTokenUsageJson,
} from "@autobe/interface";
import {
AutoBeProcessAggregateFactory,
StringUtil,
TokenUsageComputer,
} from "@autobe/utils";
import { APIError, BadRequestError } from "openai";
import { Semaphore, Singleton } from "tstl";
import typia from "typia";
import { v7 } from "uuid";
import { AutoBeConfigConstant } from "../constants/AutoBeConfigConstant";
import { AutoBeContext } from "../context/AutoBeContext";
import { AutoBeState } from "../context/AutoBeState";
import { AutoBeTokenUsage } from "../context/AutoBeTokenUsage";
import { AutoBeTokenUsageComponent } from "../context/AutoBeTokenUsageComponent";
import { IAutoBeConfig } from "../structures/IAutoBeConfig";
import { IAutoBeVendor } from "../structures/IAutoBeVendor";
import { TimedConversation } from "../utils/TimedConversation";
import { forceRetry } from "../utils/forceRetry";
import { consentFunctionCall } from "./consentFunctionCall";
import { getCriticalCompiler } from "./getCriticalCompiler";
import { mergeSystemMessages } from "./mergeSystemMessages";
// import { supportFunctionCallFallback } from "./supportFunctionCallFallback";
import { supportMistral } from "./supportMistral";
export const createAutoBeContext = (props: {
vendor: IAutoBeVendor;
compiler: () => Promise<IAutoBeCompiler>;
compilerListener: IAutoBeCompilerListener;
config: IAutoBeConfig;
state: () => AutoBeState;
files: (options: IAutoBeGetFilesOptions) => Promise<Record<string, string>>;
histories: () => AutoBeHistory[];
usage: () => AutoBeTokenUsage;
dispatch: (event: AutoBeEvent) => Promise<void>;
aggregates: AutoBeProcessAggregateCollection;
}): AutoBeContext => {
const config: Required<Omit<IAutoBeConfig, "backoffStrategy" | "timezone">> =
{
retry: props.config.retry ?? AutoBeConfigConstant.VALIDATION_RETRY,
locale: props.config.locale ?? "en-US",
timeout: props.config.timeout ?? AutoBeConfigConstant.TIMEOUT,
};
const critical: Semaphore = new Semaphore(2);
return {
vendor: props.vendor,
retry: config.retry,
locale: config.locale,
aggregates: props.aggregates,
compilerListener: props.compilerListener,
compiler: async () => {
const compiler = await props.compiler();
return getCriticalCompiler(critical, compiler);
},
files: props.files,
histories: props.histories,
state: props.state,
usage: props.usage,
dispatch: createDispatch(props),
assistantMessage: (message) => {
props.histories().push(message);
setTimeout(() => {
void props.dispatch(message).catch(() => {});
});
return message;
},
conversate: async (next, closure): Promise<AutoBeContext.IResult> => {
const aggregate: AutoBeProcessAggregate =
AutoBeProcessAggregateFactory.createAggregate();
const progress: IProgress = {
request: 0,
response: 0,
timeout: 0,
};
const metric = (key: keyof AutoBeFunctionCallingMetric): void => {
const accumulate = (collection: AutoBeProcessAggregateCollection) => {
++collection.total.metric[key];
collection[next.source as "analyzeWriteModule"] ??=
AutoBeProcessAggregateFactory.createAggregate();
++collection[next.source as "analyzeWriteModule"]!.metric[key];
};
++aggregate.metric[key];
accumulate(props.aggregates);
};
const consume = (tokenUsage: IAutoBeTokenUsageJson.IComponent): void => {
const accumulate = (
collection: AutoBeProcessAggregateCollection,
): void => {
TokenUsageComputer.increment(collection.total.tokenUsage, tokenUsage);
collection[next.source as "analyzeWriteModule"] ??=
AutoBeProcessAggregateFactory.createAggregate();
TokenUsageComputer.increment(
collection[next.source as "analyzeWriteModule"]!.tokenUsage,
tokenUsage,
);
};
TokenUsageComputer.increment(aggregate.tokenUsage, tokenUsage);
accumulate(props.aggregates);
props
.usage()
.record(tokenUsage, [
STAGES.find((stage) => next.source.startsWith(stage)) ?? "analyze",
]);
};
const execute = async (): Promise<AutoBeContext.IResult> => {
// CREATE AGENT
const agent: MicroAgentica = new MicroAgentica({
vendor: props.vendor,
config: {
...(props.config ?? {}),
executor: {
describe: false,
},
retry:
next.retry ??
props.config?.retry ??
AutoBeConfigConstant.VALIDATION_RETRY,
// stream: false,
stream: next.enforceFunctionCall === false,
} satisfies IMicroAgenticaConfig,
histories: next.histories,
controllers: [next.controller],
});
supportMistral(agent, props.vendor);
// supportFunctionCallFallback(agent, props.vendor);
mergeSystemMessages(agent, props.vendor);
// ADD EVENT LISTENERS
agent.on("request", async (event): Promise<void> => {
if (
next.enforceFunctionCall === true &&
!!event.body.tools?.length &&
(props.vendor.useToolChoice ?? true) === true
)
event.body.tool_choice = "required";
else if (event.body.tool_choice !== undefined)
delete event.body.tool_choice;
if (event.body.parallel_tool_calls !== undefined)
delete event.body.parallel_tool_calls;
if (next.promptCacheKey)
event.body.prompt_cache_key = next.promptCacheKey;
// event.body.max_tokens = 32768; // for deepseek v3.1
await props.dispatch({
...event,
type: "vendorRequest",
source: next.source,
retry: progress.request++,
});
});
agent.on("response", (event) => {
void props
.dispatch({
...event,
type: "vendorResponse",
source: next.source,
retry: progress.response++,
})
.catch(() => {});
});
agent.on("call", () => {
metric("attempt");
});
agent.on("jsonParseError", (event) => {
metric("invalidJson");
void props
.dispatch({
...event,
function: event.operation.function.name,
source: next.source,
})
.catch(() => {});
});
agent.on("validate", (event) => {
metric("validationFailure");
void props
.dispatch({
type: "jsonValidateError",
id: v7(),
source: next.source,
function: event.operation.function.name,
result: event.result,
life: event.life,
created_at: event.created_at,
})
.catch(() => {});
});
if (closure) closure(agent);
// DO CONVERSATE
const message: string =
next.enforceFunctionCall === true
? StringUtil.trim`
${next.userMessage}
> You have to call function(s) of below to accomplish my request.
>
> Never hesitate the function calling. Never ask for me permission
> to execute the function. Never explain me your plan with waiting
> for my approval.
>
> I gave you every information for the function calling, so just
> call it. I repeat that, never hesitate the function calling.
> Just do it without any explanation.
>
${next.controller.application.functions
.map((f) => `> - ${f.name}`)
.join("\n")}
`
: next.userMessage;
const result: TimedConversation.IResult =
await TimedConversation.process({
timeout: config.timeout,
agent,
message,
});
const tokenUsage: IAutoBeTokenUsageJson.IComponent = agent
.getTokenUsage()
.toJSON().aggregate;
props
.usage()
.record(tokenUsage, [
STAGES.find((stage) => next.source.startsWith(stage)) ?? "analyze",
]);
consume(tokenUsage);
const success = (histories: MicroAgenticaHistory[]) => {
metric("success");
return {
histories,
tokenUsage: aggregate.tokenUsage,
metric: aggregate.metric,
__agent: agent,
};
};
if (result.type === "error") throw result.error;
else if (result.type === "timeout") {
void props
.dispatch({
type: "vendorTimeout",
id: v7(),
source: next.source,
timeout: config.timeout!,
retry: progress.timeout++,
created_at: new Date().toISOString(),
})
.catch(() => {});
throw result.error;
} else if (
true === next.enforceFunctionCall &&
false === result.histories.some((h) => h.type === "execute")
) {
const failure = () => {
throw new Error(
StringUtil.trim`
Failed to function calling in the ${next.source} step.
Here is the list of history types that occurred during the conversation:
${result.histories.map((h) => `- ${h.type}`).join("\n")}
\`\`\`json
${JSON.stringify(result.histories, null, 2)}
\`\`\`
`,
);
};
const last: MicroAgenticaHistory | undefined =
result.histories.at(-1);
if (
last?.type === "assistantMessage" ||
(result.histories.length === 1 && last?.type === "userMessage")
) {
metric("consent");
const consent: string | null = await consentFunctionCall({
source: next.source,
dispatch: (e) => {
props.dispatch(e).catch(() => {});
},
config: props.config,
vendor: props.vendor,
assistantMessage:
last?.type === "assistantMessage" ? last.text.trim() : "",
});
if (consent !== null) {
const newHistories: MicroAgenticaHistory[] =
await agent.conversate(consent);
const newTokenUsage: IAutoBeTokenUsageJson.IComponent =
AutoBeTokenUsageComponent.minus(
new AutoBeTokenUsageComponent(
agent.getTokenUsage().toJSON().aggregate,
),
new AutoBeTokenUsageComponent(tokenUsage),
);
consume(newTokenUsage);
if (newHistories.some((h) => h.type === "execute"))
return success(newHistories);
}
}
// Retry with explicit failure feedback
const functionNames: string = next.controller.application.functions
.map((f) => f.name)
.join(", ");
for (
let retry = 0;
retry < AutoBeConfigConstant.FUNCTION_CALLING_RETRY - 1;
retry++
) {
metric("consent");
const retryMessage: string =
`You failed to call any function. ` +
`You MUST call one of these functions immediately: ${functionNames}. ` +
`Do not explain anything. Just call the function right now.`;
const retryHistories: MicroAgenticaHistory[] =
await agent.conversate(retryMessage);
const retryTokenUsage: IAutoBeTokenUsageJson.IComponent =
AutoBeTokenUsageComponent.minus(
new AutoBeTokenUsageComponent(
agent.getTokenUsage().toJSON().aggregate,
),
new AutoBeTokenUsageComponent(tokenUsage),
);
consume(retryTokenUsage);
if (retryHistories.some((h) => h.type === "execute"))
return success(retryHistories);
}
failure();
}
return success(result.histories);
};
return await forceRetry(
execute,
AutoBeConfigConstant.API_ERROR_RETRY,
(error) => {
// Context overflow and other permanent 400 errors should not be
// retried — the same payload will always produce the same failure.
if (error instanceof BadRequestError) {
const errBody = error as unknown as {
error?: { metadata?: { raw?: string }; message?: string };
};
const msg = String(
errBody.error?.metadata?.raw ??
errBody.error?.message ??
error.message ??
"",
);
const permanent = [
"context_length_exceeded",
"maximum context length",
"request too large",
];
if (permanent.some((p) => msg.includes(p))) return false;
}
return (
error instanceof APIError ||
error instanceof BadRequestError ||
error instanceof AgenticaJsonParseError ||
error instanceof AgenticaValidationError ||
error instanceof TypeError ||
(error instanceof Error &&
error.message.startsWith("OpenRouter upstream error")) ||
(error instanceof Error &&
OPENAI_API_ERROR_KEYS.get().every((key) =>
error.hasOwnProperty(key),
))
);
},
);
},
getCurrentAggregates: (phase) => {
const previous: AutoBeProcessAggregateCollection =
AutoBeProcessAggregateFactory.reduce(
props
.histories()
.filter(
(h) =>
h.type === "analyze" ||
h.type === "database" ||
h.type === "interface" ||
h.type === "test" ||
h.type === "realize",
)
.map((h) => h.aggregates),
);
return AutoBeProcessAggregateFactory.filterPhase(
AutoBeProcessAggregateFactory.minus(props.aggregates, previous),
phase,
);
},
};
};
const createDispatch = (props: {
state: () => AutoBeState;
histories: () => AutoBeHistory[];
dispatch: (event: AutoBeEvent) => Promise<void>;
}) => {
let analyzeStart: AutoBeAnalyzeStartEvent | null = null;
let databaseStart: AutoBeDatabaseStartEvent | null = null;
let interfaceStart: AutoBeInterfaceStartEvent | null = null;
let testStart: AutoBeTestStartEvent | null = null;
let realizeStart: AutoBeRealizeStartEvent | null = null;
return <Event extends Exclude<AutoBeEvent, AutoBeAssistantMessageEvent>>(
event: Event,
): AutoBeContext.DispatchHistory<Event> => {
// starts
if (event.type === "analyzeStart") analyzeStart = event;
else if (event.type === "databaseStart") databaseStart = event;
else if (event.type === "interfaceStart") interfaceStart = event;
else if (event.type === "testStart") testStart = event;
else if (event.type === "realizeStart") realizeStart = event;
// completes
else if (event.type === "analyzeComplete")
return transformAndDispatch<AutoBeAnalyzeCompleteEvent>({
dispatch: props.dispatch,
histories: props.histories,
state: props.state,
event,
history: {
type: "analyze",
id: v7(),
prefix: event.prefix,
actors: event.actors,
files: event.files,
aggregates: event.aggregates,
step: event.step,
created_at: analyzeStart?.created_at ?? new Date().toISOString(),
completed_at: event.created_at,
} satisfies AutoBeAnalyzeHistory,
}) as AutoBeContext.DispatchHistory<Event>;
else if (event.type === "databaseComplete")
return transformAndDispatch<AutoBeDatabaseCompleteEvent>({
dispatch: props.dispatch,
histories: props.histories,
state: props.state,
event,
history: {
type: "database",
id: v7(),
instruction: databaseStart?.reason ?? "",
schemas: event.schemas,
result: event.result,
compiled: event.compiled,
aggregates: event.aggregates,
step: event.step,
created_at: databaseStart?.created_at ?? new Date().toISOString(),
completed_at: event.created_at,
} satisfies AutoBeDatabaseHistory,
}) as AutoBeContext.DispatchHistory<Event>;
else if (event.type === "interfaceComplete")
return transformAndDispatch({
dispatch: props.dispatch,
histories: props.histories,
state: props.state,
event,
history: {
type: "interface",
id: v7(),
instruction: interfaceStart?.reason ?? "",
authorizations: event.authorizations,
document: event.document,
missed: event.missed,
aggregates: event.aggregates,
step: event.step,
created_at: interfaceStart?.created_at ?? new Date().toISOString(),
completed_at: new Date().toISOString(),
} satisfies AutoBeInterfaceHistory,
}) as AutoBeContext.DispatchHistory<Event>;
else if (event.type === "testComplete")
return transformAndDispatch<AutoBeTestCompleteEvent>({
dispatch: props.dispatch,
histories: props.histories,
state: props.state,
event,
history: {
type: "test",
id: v7(),
instruction: testStart?.reason ?? "",
functions: event.functions,
compiled: event.compiled,
aggregates: event.aggregates,
step: event.step,
created_at: testStart?.created_at ?? new Date().toISOString(),
completed_at: new Date().toISOString(),
} satisfies AutoBeTestHistory,
}) as AutoBeContext.DispatchHistory<Event>;
else if (event.type === "realizeComplete")
return transformAndDispatch<AutoBeRealizeCompleteEvent>({
dispatch: props.dispatch,
histories: props.histories,
state: props.state,
event,
history: {
type: "realize",
id: v7(),
instruction: realizeStart?.reason ?? "",
authorizations: event.authorizations,
functions: event.functions,
controllers: event.controllers,
compiled: event.compiled,
aggregates: event.aggregates,
step: event.step,
created_at: realizeStart?.created_at ?? new Date().toISOString(),
completed_at: new Date().toISOString(),
} satisfies AutoBeRealizeHistory,
}) as AutoBeContext.DispatchHistory<Event>;
void props.dispatch(event).catch(() => {});
return null as AutoBeContext.DispatchHistory<Event>;
};
};
const transformAndDispatch = <
Event extends
| AutoBeAnalyzeCompleteEvent
| AutoBeDatabaseCompleteEvent
| AutoBeInterfaceCompleteEvent
| AutoBeTestCompleteEvent
| AutoBeRealizeCompleteEvent,
>(props: {
dispatch: (event: Event) => Promise<void>;
histories: () => AutoBeHistory[];
state: () => AutoBeState;
event: Event;
history: NonNullable<AutoBeContext.DispatchHistory<Event>>;
}): NonNullable<AutoBeContext.DispatchHistory<Event>> => {
props.histories().push(props.history);
// biome-ignore lint: intended
props.state()[props.history.type] = props.history as any;
void props.dispatch(props.event).catch(() => {});
return props.history;
};
const STAGES =
typia.misc.literals<
keyof Pick<IAutoBeTokenUsageJson, "facade" | AutoBePhase>
>();
const OPENAI_API_ERROR_KEYS = new Singleton(() =>
Object.keys(new APIError(undefined, undefined, undefined, undefined)),
);
interface IProgress {
request: number;
response: number;
timeout: number;
}