@autobe/agent
Version:
AI backend server code generator
503 lines (489 loc) • 17.9 kB
text/typescript
import { MicroAgentica, MicroAgenticaHistory } from "@agentica/core";
import {
AutoBeAnalyzeCompleteEvent,
AutoBeAnalyzeHistory,
AutoBeAnalyzeStartEvent,
AutoBeAssistantMessageEvent,
AutoBeEvent,
AutoBeFunctionCallingMetric,
AutoBeHistory,
AutoBeInterfaceCompleteEvent,
AutoBeInterfaceHistory,
AutoBeInterfaceStartEvent,
AutoBePrismaCompleteEvent,
AutoBePrismaHistory,
AutoBePrismaStartEvent,
AutoBeProcessAggregate,
AutoBeProcessAggregateCollection,
AutoBeRealizeCompleteEvent,
AutoBeRealizeHistory,
AutoBeRealizeStartEvent,
AutoBeTestCompleteEvent,
AutoBeTestHistory,
AutoBeTestStartEvent,
IAutoBeCompiler,
IAutoBeCompilerListener,
IAutoBeGetFilesOptions,
IAutoBeTokenUsageJson,
} from "@autobe/interface";
import {
AutoBeProcessAggregateFactory,
StringUtil,
TokenUsageComputer,
} from "@autobe/utils";
import { ILlmSchema } from "@samchon/openapi";
import { Semaphore } 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 { IAutoBeFacadeApplication } from "../orchestrate/facade/histories/IAutoBeFacadeApplication";
import { IAutoBeConfig } from "../structures/IAutoBeConfig";
import { IAutoBeVendor } from "../structures/IAutoBeVendor";
import { AutoBeTimeoutError } from "../utils/AutoBeTimeoutError";
import { TimedConversation } from "../utils/TimedConversation";
import { consentFunctionCall } from "./consentFunctionCall";
import { getCommonPrompt } from "./getCommonPrompt";
import { getCriticalCompiler } from "./getCriticalCompiler";
import { supportMistral } from "./supportMistral";
export const createAutoBeContext = <Model extends ILlmSchema.Model>(props: {
model: Model;
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<Model> => {
const config: Required<Omit<IAutoBeConfig, "backoffStrategy" | "timezone">> =
{
retry: props.config.retry ?? AutoBeConfigConstant.RETRY,
locale: props.config.locale ?? "en-US",
timeout: props.config.timeout ?? null,
};
const critical: Semaphore = new Semaphore(2);
return {
model: props.model,
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) => {
const aggregate: AutoBeProcessAggregate =
AutoBeProcessAggregateFactory.createAggregate();
const metric = (key: keyof AutoBeFunctionCallingMetric) => {
const accumulate = (collection: AutoBeProcessAggregateCollection) => {
++collection.total.metric[key];
collection[next.source as "analyzeWrite"] ??=
AutoBeProcessAggregateFactory.createAggregate();
++collection[next.source as "analyzeWrite"]!.metric[key];
};
++aggregate.metric[key];
accumulate(props.aggregates);
};
const consume = (tokenUsage: IAutoBeTokenUsageJson.IComponent) => {
const accumulate = (collection: AutoBeProcessAggregateCollection) => {
TokenUsageComputer.increment(collection.total.tokenUsage, tokenUsage);
collection[next.source as "analyzeWrite"] ??=
AutoBeProcessAggregateFactory.createAggregate();
TokenUsageComputer.increment(
collection[next.source as "analyzeWrite"]!.tokenUsage,
tokenUsage,
);
};
TokenUsageComputer.increment(aggregate.tokenUsage, tokenUsage);
accumulate(props.aggregates);
props
.usage()
.record(tokenUsage, [
STAGES.find((stage) => next.source.startsWith(stage)) ?? "analyze",
]);
};
const progress = {
request: 0,
response: 0,
timeout: 0,
};
const execute = async (): Promise<AutoBeContext.IResult<Model>> => {
// CREATE AGENT
const agent: MicroAgentica<Model> = new MicroAgentica<Model>({
model: props.model,
vendor: props.vendor,
config: {
...(props.config ?? {}),
retry: props.config?.retry ?? AutoBeConfigConstant.RETRY,
executor: {
describe: null,
},
systemPrompt: {
common: () => getCommonPrompt(props.config),
},
},
histories: next.histories,
controllers: [next.controller],
});
supportMistral(agent, props.vendor);
// ADD EVENT LISTENERS
agent.on("request", async (event) => {
if (next.enforceFunctionCall === true && event.body.tools)
event.body.tool_choice = "required";
if (event.body.parallel_tool_calls !== undefined)
delete event.body.parallel_tool_calls;
if (next.promptCacheKey)
event.body.prompt_cache_key = next.promptCacheKey;
await props.dispatch({
...event,
type: "vendorRequest",
source: next.source,
retry: progress.request++,
});
});
agent.on("response", async (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<Model> =
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<Model>[]) => {
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" && h.success === true,
)
) {
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.stringify(result.histories)}
`,
);
};
const last: MicroAgenticaHistory<Model> | undefined =
result.histories.at(-1);
if (
last?.type === "assistantMessage" &&
last.text.trim().length !== 0
) {
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.text,
});
if (consent !== null) {
const newHistories: MicroAgenticaHistory<Model>[] =
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" && h.success))
return success(newHistories);
}
}
failure();
}
return success(result.histories);
};
if (next.enforceFunctionCall === true)
return await forceRetry(execute, config.retry);
else return await execute();
},
getCurrentAggregates: (phase) => {
const previous: AutoBeProcessAggregateCollection =
AutoBeProcessAggregateFactory.reduce(
props
.histories()
.filter(
(h) =>
h.type === "analyze" ||
h.type === "prisma" ||
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 prismaStart: AutoBePrismaStartEvent | 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 === "prismaStart") prismaStart = 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 === "prismaComplete")
return transformAndDispatch<AutoBePrismaCompleteEvent>({
dispatch: props.dispatch,
histories: props.histories,
state: props.state,
event,
history: {
type: "prisma",
id: v7(),
instruction: prismaStart?.reason ?? "",
schemas: event.schemas,
result: event.result,
compiled: event.compiled,
aggregates: event.aggregates,
step: event.step,
created_at: prismaStart?.created_at ?? new Date().toISOString(),
completed_at: event.created_at,
} satisfies AutoBePrismaHistory,
}) 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 ?? "",
files: event.files,
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
| AutoBePrismaCompleteEvent
| 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);
props.state()[props.history.type] = props.history as any;
void props.dispatch(props.event).catch(() => {});
return props.history;
};
const forceRetry = async <T>(
task: () => Promise<T>,
count: number,
): Promise<T> => {
let error: unknown = undefined;
for (let i: number = 0; i < count; ++i)
try {
return await task();
} catch (e) {
if (e instanceof AutoBeTimeoutError) throw e;
error = e;
}
throw error;
};
const STAGES = typia.misc.literals<keyof IAutoBeFacadeApplication>();