@autobe/agent
Version:
AI backend server code generator
765 lines (732 loc) • 25.4 kB
text/typescript
import {
IAgenticaHistoryJson,
IMicroAgenticaHistoryJson,
} from "@agentica/core";
import {
AutoBeAnalyzeHistory,
AutoBeDatabase,
AutoBeEventSource,
AutoBeOpenApi,
AutoBePreliminaryKind,
AutoBeRealizeCollectorFunction,
AutoBeRealizeTransformerFunction,
} from "@autobe/interface";
import {
AutoBeOpenApiEndpointComparator,
StringUtil,
writePrismaApplication,
} from "@autobe/utils";
import { HashSet } from "tstl";
import { v7 } from "uuid";
import { AutoBeConfigConstant } from "../../../constants/AutoBeConfigConstant";
import { AutoBeSystemPromptConstant } from "../../../constants/AutoBeSystemPromptConstant";
import { AutoBeState } from "../../../context/AutoBeState";
import { AutoBeInterfaceSchemaProgrammer } from "../../interface/programmers/AutoBeInterfaceSchemaProgrammer";
import { AutoBePreliminaryController } from "../AutoBePreliminaryController";
import { IAutoBePreliminaryRequest } from "../structures/AutoBePreliminaryRequest";
import { IAnalysisSectionEntry } from "../structures/IAnalysisSectionEntry";
import { IAutoBePreliminaryCollection } from "../structures/IAutoBePreliminaryCollection";
export const transformPreliminaryHistory = <Kind extends AutoBePreliminaryKind>(
preliminary: AutoBePreliminaryController<Kind>,
): IMicroAgenticaHistoryJson[] => {
const histories: IMicroAgenticaHistoryJson[] = preliminary
.getKinds()
.map((key): IMicroAgenticaHistoryJson[] => {
const newKey: string = key.startsWith("previous")
? key.replace("previous", "")
: key;
const type: Exclude<AutoBePreliminaryKind, `previous${string}`> = (newKey
.slice(0, 1)
.toLowerCase() + newKey.slice(1)) as Exclude<
AutoBePreliminaryKind,
`previous${string}`
>;
return PreliminaryTransformer[type]({
source: preliminary.getSource(),
state: preliminary.getState(),
all: preliminary.getAll() as IAutoBePreliminaryCollection,
local: preliminary.getLocal() as IAutoBePreliminaryCollection,
// biome-ignore lint: intended
config: preliminary.getConfig() as any,
previous: key.startsWith("previous"),
analysisPageOffset: preliminary.getAnalysisPageOffset(),
});
})
.flat();
// sequence messages
const systems: IAgenticaHistoryJson.ISystemMessage[] = histories.filter(
(h) => h.type === "systemMessage",
);
const others: IMicroAgenticaHistoryJson[] = histories.filter(
(h) => h.type !== "systemMessage",
);
const messages: IMicroAgenticaHistoryJson[] = [...systems, ...others];
// previous written value
const previousWrite: Record<string, unknown> | null =
preliminary.getPreviousWrite();
if (previousWrite !== null)
messages.push(
createFunctionCallingMessage({
controller: preliminary.getSource(),
kind: "write",
arguments: previousWrite,
}),
);
return messages;
};
namespace PreliminaryTransformer {
export interface IProps<Kind extends AutoBePreliminaryKind> {
source: Exclude<AutoBeEventSource, "facade" | "preliminaryAcquire">;
state: AutoBeState;
all: Pick<IAutoBePreliminaryCollection, Kind>;
local: Pick<IAutoBePreliminaryCollection, Kind>;
config: AutoBePreliminaryController.IConfig<Kind>;
previous: boolean;
analysisPageOffset: number;
}
export const analysisSections = (
props: IProps<"analysisSections" | "previousAnalysisSections">,
): IMicroAgenticaHistoryJson[] => {
const kind: "analysisSections" | "previousAnalysisSections" = props.previous
? "previousAnalysisSections"
: "analysisSections";
const oldbie: Map<number, IAnalysisSectionEntry> = new Map(
props.local[kind]
.map((s) => [s.id, s] as const)
.sort(([a], [b]) => a - b),
);
const newbie: IAnalysisSectionEntry[] = props.all[kind]
.filter((s) => oldbie.has(s.id) === false)
.sort((a, b) => a.id - b.id);
const analyze: AutoBeAnalyzeHistory | null = props.previous
? props.state.previousAnalyze
: props.state.analyze;
const assistant: IAgenticaHistoryJson.IAssistantMessage =
createAssistantMessage({
prompt:
AutoBeSystemPromptConstant.PRELIMINARY_ANALYSIS_SECTION_LOADED.replace(
"{{PREFIX}}",
analyze?.prefix ?? "",
).replace(
"{{ACTORS}}",
analyze?.actors ? toJsonBlock(analyze.actors) : "",
),
previous:
AutoBeSystemPromptConstant.PRELIMINARY_ANALYSIS_SECTION_PREVIOUS,
content: Array.from(oldbie.values())
.map(
(s) =>
`### [ID: ${s.id}] ${s.filename} > ${s.unitTitle} > ${s.sectionTitle}\n\n${s.content}`,
)
.join("\n\n---\n\n"),
replace: props.previous
? { from: "getAnalysisSections", to: "getPreviousAnalysisSections" }
: null,
});
const pageSize: number = AutoBeConfigConstant.ANALYSIS_PAGE_SIZE;
const pageStart: number = props.analysisPageOffset;
const page: IAnalysisSectionEntry[] = newbie.slice(
pageStart,
pageStart + pageSize,
);
const totalAvailable: number = newbie.length;
const paginationNote: string =
totalAvailable > pageStart + pageSize
? `\n\n(Showing ${page.length} of ${totalAvailable} available sections, starting from offset ${pageStart}. More sections will appear after you request some of the above.)`
: "";
const system: IAgenticaHistoryJson.ISystemMessage = createSystemMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_ANALYSIS_SECTION,
previous:
AutoBeSystemPromptConstant.PRELIMINARY_ANALYSIS_SECTION_PREVIOUS,
available: formatCompactSectionIndex(page) + paginationNote,
loaded: Array.from(oldbie.values())
.map((s) => `- [${s.id}] ${s.sectionTitle}`)
.join("\n"),
exhausted:
page.length === 0
? AutoBeSystemPromptConstant.PRELIMINARY_ANALYSIS_SECTION_EXHAUSTED
: "",
replace: props.previous
? {
from: "getAnalysisSections",
to: "getPreviousAnalysisSections",
}
: null,
});
return props.local[kind].length === 0
? [assistant, system]
: [
createFunctionCallingMessage({
controller: props.source,
kind,
arguments: {
thinking: "analysis sections for detailed requirements' analyses",
request: {
type: props.previous
? "getPreviousAnalysisSections"
: "getAnalysisSections",
sectionIds: props.local[kind].map((s) => s.id),
},
},
}),
assistant,
system,
];
};
export const databaseSchemas = (
props: IProps<"databaseSchemas" | "previousDatabaseSchemas">,
): IMicroAgenticaHistoryJson[] => {
const kind: "databaseSchemas" | "previousDatabaseSchemas" = props.previous
? "previousDatabaseSchemas"
: "databaseSchemas";
const oldbie: Record<string, AutoBeDatabase.IModel> = Object.fromEntries(
props.local[kind]
.map((s) => [s.name, s] as const)
.sort(([a], [b]) => a.localeCompare(b)),
);
const newbie: AutoBeDatabase.IModel[] = props.all[kind]
.filter((s) => oldbie[s.name] === undefined)
.sort((a, b) => a.name.localeCompare(b.name));
const assistant: IAgenticaHistoryJson.IAssistantMessage =
createAssistantMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_DATABASE_SCHEMA_LOADED,
previous:
AutoBeSystemPromptConstant.PRELIMINARY_DATABASE_SCHEMA_PREVIOUS,
content:
props.config.database === "ast"
? StringUtil.trim`
## Database AST Data
${toJsonBlock(oldbie)}
`
: StringUtil.trim`
## Database Schema Files
\`\`\`prisma
${
writePrismaApplication({
dbms: "postgres",
application: {
files: [
{
filename: "all.prisma",
namespace: "All",
models: Object.values(oldbie),
},
],
},
})["all.prisma"]
}
\`\`\
`,
replace: props.previous
? {
from: "getDatabaseSchemas",
to: "getPreviousDatabaseSchemas",
}
: null,
});
if (props.config.databaseProperty === true)
assistant.text +=
"\n\n" +
StringUtil.trim`
### Database Schema Properties
\`\`\`json
${JSON.stringify(
Object.fromEntries(
Object.entries(oldbie).map(([k, v]) => [
k,
AutoBeInterfaceSchemaProgrammer.getDatabaseSchemaProperties({
everyModels: props.all.databaseSchemas,
model: v,
}).map((p) => p.key),
]),
),
)}
\`\`\`
`;
const db: AutoBeDatabase.IApplication | undefined = props.previous
? props.state.previousDatabase?.result.data
: props.state.database?.result.data;
const system: IAgenticaHistoryJson.ISystemMessage = createSystemMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_DATABASE_SCHEMA,
previous: AutoBeSystemPromptConstant.PRELIMINARY_DATABASE_SCHEMA_PREVIOUS,
available: StringUtil.trim`
Name | Belonged Group | Stance | Summary
-----|----------------|--------|---------
${newbie
.map((m) =>
[
m.name,
db?.files.find((f) => f.models.some((md) => md.name === m.name))
?.namespace ?? "-",
m.stance,
StringUtil.summary(m.description),
].join(" | "),
)
.join("\n")}
`,
loaded: props.local[kind].map((s) => `- ${s.name}`).join("\n"),
exhausted:
newbie.length === 0
? AutoBeSystemPromptConstant.PRELIMINARY_DATABASE_SCHEMA_EXHAUSTED
: "",
replace: props.previous
? {
from: "getDatabaseSchemas",
to: "getPreviousDatabaseSchemas",
}
: null,
});
return props.local[kind].length === 0
? [assistant, system]
: [
createFunctionCallingMessage({
controller: props.source,
kind,
arguments: {
thinking: "database schemas for DB schema information",
request: {
type: props.previous
? "getPreviousDatabaseSchemas"
: "getDatabaseSchemas",
schemaNames: props.local[kind].map((s) => s.name),
},
},
}),
assistant,
system,
];
};
export const interfaceOperations = (
props: IProps<"interfaceOperations" | "previousInterfaceOperations">,
): IMicroAgenticaHistoryJson[] => {
const kind: "interfaceOperations" | "previousInterfaceOperations" =
props.previous ? "previousInterfaceOperations" : "interfaceOperations";
const oldbie: HashSet<AutoBeOpenApi.IEndpoint> = new HashSet(
props.local[kind]
.map((o) => ({
method: o.method,
path: o.path,
}))
.sort(AutoBeOpenApiEndpointComparator.compare),
AutoBeOpenApiEndpointComparator.hashCode,
AutoBeOpenApiEndpointComparator.equals,
);
const newbie: AutoBeOpenApi.IOperation[] = props.all[kind]
.filter(
(o) =>
oldbie.has({
method: o.method,
path: o.path,
}) === false,
)
.sort(AutoBeOpenApiEndpointComparator.compare);
const assistant: IAgenticaHistoryJson.IAssistantMessage =
createAssistantMessage({
prompt:
AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_OPERATION_LOADED,
previous:
AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_OPERATION_PREVIOUS,
content: toJsonBlock(props.local[kind]),
replace: props.previous
? {
from: "getInterfaceOperations",
to: "getPreviousInterfaceOperations",
}
: null,
});
const system: IAgenticaHistoryJson.ISystemMessage = createSystemMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_OPERATION,
previous:
AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_OPERATION_PREVIOUS,
available: StringUtil.trim`
Method | Path | Actor? | Authorization? | Summary
-------|------|--------|----------------|---------
${newbie
.map((o) =>
[
o.method,
o.path,
o.authorizationActor ?? "-",
o.authorizationType ?? "-",
StringUtil.summary(o.description),
].join(" | "),
)
.join("\n")}
`,
loaded: StringUtil.trim`
Method | Path
-------|-------
${oldbie
.toJSON()
.map((e) => [e.method, e.path].join(" | "))
.join("\n")}
`,
exhausted:
newbie.length === 0
? AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_OPERATION_EXHAUSTED
: "",
replace: props.previous
? {
from: "getInterfaceOperations",
to: "getPreviousInterfaceOperations",
}
: null,
});
return props.local[kind].length === 0
? [assistant, system]
: [
createFunctionCallingMessage({
controller: props.source,
kind,
arguments: {
thinking:
"interface operations for detailed endpoint information",
request: {
type: props.previous
? "getPreviousInterfaceOperations"
: "getInterfaceOperations",
endpoints: oldbie.toJSON(),
},
},
}),
assistant,
system,
];
};
export const interfaceSchemas = (
props: IProps<"interfaceSchemas" | "previousInterfaceSchemas">,
): IMicroAgenticaHistoryJson[] => {
const kind: "interfaceSchemas" | "previousInterfaceSchemas" = props.previous
? "previousInterfaceSchemas"
: "interfaceSchemas";
const newbie: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive> = {};
for (const [k, v] of Object.entries(props.all[kind]).sort(([a], [b]) =>
a.localeCompare(b),
))
if (props.local[kind][k] === undefined) newbie[k] = v;
const assistant: IAgenticaHistoryJson.IAssistantMessage =
createAssistantMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_SCHEMA_LOADED,
previous:
AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_SCHEMA_PREVIOUS,
content: toJsonBlock(props.local[kind]),
replace: props.previous
? {
from: "getInterfaceSchemas",
to: "getPreviousInterfaceSchemas",
}
: null,
});
const system: IAgenticaHistoryJson.ISystemMessage = createSystemMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_SCHEMA,
previous:
AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_SCHEMA_PREVIOUS,
available: StringUtil.trim`
Name | Summary
-----|---------
${Object.entries(newbie)
.map(([name, schema]) =>
[name, StringUtil.summary(schema.description)].join(" | "),
)
.join("\n")}
`,
loaded: Object.keys(props.local[kind])
.sort()
.map((k) => `- ${k}`)
.join("\n"),
exhausted:
Object.keys(newbie).length === 0
? AutoBeSystemPromptConstant.PRELIMINARY_INTERFACE_SCHEMA_EXHAUSTED
: "",
replace: props.previous
? {
from: "getInterfaceSchemas",
to: "getPreviousInterfaceSchemas",
}
: null,
});
return Object.keys(props.local[kind]).length === 0
? [assistant, system]
: [
createFunctionCallingMessage({
controller: props.source,
kind,
arguments: {
thinking: "interface schemas for detailed schema information",
request: {
type: props.previous
? "getPreviousInterfaceSchemas"
: "getInterfaceSchemas",
typeNames: Object.keys(props.local[kind]),
},
},
}),
assistant,
system,
];
};
export const realizeCollectors = (
props: IProps<"realizeCollectors">,
): IMicroAgenticaHistoryJson[] => {
const oldbie: Record<string, AutoBeRealizeCollectorFunction> =
Object.fromEntries(
props.local.realizeCollectors
.map((c) => [c.plan.dtoTypeName, c] as const)
.sort(([a], [b]) => a.localeCompare(b)),
);
const newbie: AutoBeRealizeCollectorFunction[] = props.all.realizeCollectors
.filter((c) => oldbie[c.plan.dtoTypeName] === undefined)
.sort((a, b) => a.plan.dtoTypeName.localeCompare(b.plan.dtoTypeName));
const assistant: IAgenticaHistoryJson.IAssistantMessage =
createAssistantMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_REALIZE_COLLECTOR_LOADED,
content: toJsonBlock(oldbie),
replace: null,
previous: null,
});
const system: IAgenticaHistoryJson.ISystemMessage = createSystemMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_REALIZE_COLLECTOR,
available: StringUtil.trim`
DTO Type Name | Database Table | References | Neighbor Collectors
--------------|----------------|------------|--------------------
${newbie
.map((c) =>
[
c.plan.dtoTypeName,
c.plan.databaseSchemaName,
c.plan.references.length > 0
? `(${c.plan.references.map((r) => r.source).join(", ")})`
: "-",
`(${c.neighbors.join(", ")})`,
].join(" | "),
)
.join("\n")}
`,
loaded: props.local.realizeCollectors
.map((c) => `- ${c.plan.dtoTypeName}`)
.join("\n"),
exhausted:
newbie.length === 0
? AutoBeSystemPromptConstant.PRELIMINARY_REALIZE_COLLECTOR_EXHAUSTED
: "",
replace: null,
previous: null,
});
return props.local.realizeCollectors.length === 0
? [assistant, system]
: [
createFunctionCallingMessage({
controller: props.source,
kind: "realizeCollectors",
arguments: {
thinking: "realize collectors for Create DTO transformation",
request: {
type: "getRealizeCollectors",
dtoTypeNames: props.local.realizeCollectors.map(
(c) => c.plan.dtoTypeName,
),
},
},
}),
assistant,
system,
];
};
export const complete = (
_props: IProps<"complete">,
): IMicroAgenticaHistoryJson[] => [];
export const realizeTransformers = (
props: IProps<"realizeTransformers">,
): IMicroAgenticaHistoryJson[] => {
const oldbie: Record<string, AutoBeRealizeTransformerFunction> =
Object.fromEntries(
props.local.realizeTransformers
.map((t) => [t.plan.dtoTypeName, t] as const)
.sort(([a], [b]) => a.localeCompare(b)),
);
const newbie: AutoBeRealizeTransformerFunction[] =
props.all.realizeTransformers
.filter((t) => oldbie[t.plan.dtoTypeName] === undefined)
.sort((a, b) => a.plan.dtoTypeName.localeCompare(b.plan.dtoTypeName));
const assistant: IAgenticaHistoryJson.IAssistantMessage =
createAssistantMessage({
prompt:
AutoBeSystemPromptConstant.PRELIMINARY_REALIZE_TRANSFORMER_LOADED,
content: toJsonBlock(oldbie),
replace: null,
previous: null,
});
const system: IAgenticaHistoryJson.ISystemMessage = createSystemMessage({
prompt: AutoBeSystemPromptConstant.PRELIMINARY_REALIZE_TRANSFORMER,
available: StringUtil.trim`
DTO Type Name | Database Table | Neighbor Transformers
--------------|----------------|----------------------
${newbie
.map((t) =>
[
t.plan.dtoTypeName,
t.plan.databaseSchemaName,
`(${t.neighbors.join(", ")})`,
].join(" | "),
)
.join("\n")}
`,
loaded: props.local.realizeTransformers
.map((t) => `- ${t.plan.dtoTypeName}`)
.join("\n"),
exhausted:
newbie.length === 0
? AutoBeSystemPromptConstant.PRELIMINARY_REALIZE_TRANSFORMER_EXHAUSTED
: "",
replace: null,
previous: null,
});
return props.local.realizeTransformers.length === 0
? [assistant, system]
: [
createFunctionCallingMessage({
controller: props.source,
kind: "realizeTransformers",
arguments: {
thinking: "realize transformers for response DTO construction",
request: {
type: "getRealizeTransformers",
dtoTypeNames: props.local.realizeTransformers.map(
(t) => t.plan.dtoTypeName,
),
},
},
}),
assistant,
system,
];
};
}
interface IPromptReplace {
from: Exclude<
// biome-ignore lint: intended
IAutoBePreliminaryRequest<any>["request"]["type"],
`getPrevious${string}`
>;
to: Extract<
// biome-ignore lint: intended
IAutoBePreliminaryRequest<any>["request"]["type"],
`getPrevious${string}`
>;
}
const createAssistantMessage = (props: {
prompt: string;
content: string;
replace: IPromptReplace | null;
previous: string | null;
}): IAgenticaHistoryJson.IAssistantMessage => {
let text = props.prompt
.replaceAll("{{CONTENT}}", props.content)
.replaceAll(
"{{PREVIOUS}}",
props.replace !== null && props.previous !== null ? props.previous : "",
);
if (props.replace !== null)
text = text.replaceAll(props.replace.from, props.replace.to);
return {
id: v7(),
type: "assistantMessage",
text,
created_at: new Date().toISOString(),
};
};
const createSystemMessage = (props: {
prompt: string;
available: string;
loaded: string;
exhausted: string;
replace: IPromptReplace | null;
previous: string | null;
}): IAgenticaHistoryJson.ISystemMessage => {
let text = props.prompt
.replaceAll("{{AVAILABLE}}", props.available)
.replaceAll("{{LOADED}}", props.loaded)
.replaceAll("{{EXHAUSTED}}", props.exhausted)
.replaceAll(
"{{PREVIOUS}}",
props.replace !== null && props.previous !== null ? props.previous : "",
);
if (props.replace !== null)
text = text.replaceAll(props.replace.from, props.replace.to);
return {
id: v7(),
type: "systemMessage",
text,
created_at: new Date().toISOString(),
};
};
const toJsonBlock = (obj: unknown): string =>
StringUtil.trim`
\`\`\`json
${JSON.stringify(obj)}
\`\`\`
`;
const formatCompactSectionIndex = (
sections: IAnalysisSectionEntry[],
): string => {
let result = "";
let lastFile = "";
let lastUnit = "";
let unitSections: string[] = [];
const flushUnit = () => {
if (unitSections.length > 0) {
result += " " + lastUnit + ": " + unitSections.join(", ") + "\n";
unitSections = [];
}
};
for (const s of sections) {
if (s.filename !== lastFile) {
flushUnit();
result += "\n[" + s.filename + "]\n";
lastFile = s.filename;
lastUnit = "";
}
if (s.unitTitle !== lastUnit) {
flushUnit();
lastUnit = s.unitTitle;
}
unitSections.push(s.id + "." + s.sectionTitle);
}
flushUnit();
return result.trim();
};
// experimenting between assistantMessage and execute types
const createFunctionCallingMessage = <
Kind extends AutoBePreliminaryKind,
>(props: {
controller: Exclude<AutoBeEventSource, "facade" | "preliminaryAcquire">;
kind: Kind | "write";
arguments: Record<string, unknown>;
}): IAgenticaHistoryJson.IAssistantMessage | IAgenticaHistoryJson.IExecute => ({
type: "execute",
id: v7(),
operation: {
protocol: "class",
controller: props.controller,
function: "process",
name: "process",
},
arguments: props.arguments,
value: undefined,
success: true,
created_at: new Date().toISOString(),
// type: "assistantMessage",
// id: v7(),
// text: StringUtil.trim`
// # Function Calling History
// Function "${props.function}()" has been called.
// Here is the arguments.
// Note that, never call the same items again.
// As they are loaded onto the memory, you never have to
// request none of them again.
// \`\`\`json
// ${JSON.stringify(props.argument)}
// \`\`\`
// `,
// created_at: new Date().toISOString(),
});