@autobe/agent
Version:
AI backend server code generator
296 lines (273 loc) • 9.41 kB
text/typescript
import { IAgenticaController, MicroAgentica } from "@agentica/core";
import {
AutoBeAnalyzeHistory,
AutoBeAssistantMessageHistory,
} from "@autobe/interface";
import { ILlmApplication, ILlmSchema } from "@samchon/openapi";
import typia from "typia";
import { v4 } from "uuid";
import { AutoBeSystemPromptConstant } from "../../constants/AutoBeSystemPromptConstant";
import { AutoBeContext } from "../../context/AutoBeContext";
import { IAutoBeApplicationProps } from "../../context/IAutoBeApplicationProps";
import { assertSchemaModel } from "../../context/assertSchemaModel";
import { enforceToolCall } from "../../utils/enforceToolCall";
import { AutoBeAnalyzeAgent } from "./AutoBeAnalyzeAgent";
import { IFile } from "./AutoBeAnalyzeFileSystem";
import { AutoBeAnalyzePointer } from "./AutoBeAnalyzePointer";
import { AutoBeAnalyzeReviewer } from "./AutoBeAnalyzeReviewer";
/** @todo Kakasoo */
export const orchestrateAnalyze =
<Model extends ILlmSchema.Model>(ctx: AutoBeContext<Model>) =>
async (
props: IAutoBeApplicationProps,
): Promise<AutoBeAssistantMessageHistory | AutoBeAnalyzeHistory> => {
const userPlanningRequirements = props.userPlanningRequirements;
if (!userPlanningRequirements) {
throw new Error(
`Unable to prepare a proposal because there is no user requirement`,
);
}
const step = ctx.state().analyze?.step ?? 0;
const created_at = new Date().toISOString();
ctx.dispatch({
type: "analyzeStart",
reason: userPlanningRequirements,
step,
created_at,
});
const controller = createController<Model>({
model: ctx.model,
execute: new DeterminingFiles(),
});
const agentica = new MicroAgentica({
model: ctx.model,
vendor: ctx.vendor,
controllers: [controller],
config: {
locale: ctx.config?.locale,
executor: {
describe: null,
},
systemPrompt: {
common: () => AutoBeSystemPromptConstant.ANALYZE_PLANNER,
},
},
histories: [
...ctx
.histories()
.filter(
(el) => el.type === "assistantMessage" || el.type === "userMessage",
),
],
});
enforceToolCall(agentica);
const determined = await agentica.conversate(
[
"Design a complete list of documents for that document",
"```md",
userPlanningRequirements,
"```",
].join("\n"),
);
const lastMessage = determined[determined.length - 1]!;
if (lastMessage.type === "assistantMessage") {
const history: AutoBeAssistantMessageHistory = {
id: v4(),
type: "assistantMessage",
text: lastMessage.text,
created_at,
completed_at: new Date().toISOString(),
};
ctx.dispatch({
type: "assistantMessage",
text: lastMessage.text,
created_at,
});
return history;
}
const described = determined.find((el) => el.type === "describe");
// const determinedOutput = Array.from(
// new Set(
// described
// ? described.executes
// .map((el) => {
// if (el.protocol === "class") {
// return el.arguments as unknown as IDeterminingInput;
// }
// return null;
// })
// .filter((el) => el !== null)
// : [],
// ),
// );
const determinedOutput = described?.executes.find(
(el) => el.protocol === "class" && typia.is<IDeterminingInput>(el.value),
)?.value as IDeterminingInput;
const prefix = determinedOutput.prefix;
const describedFiles = determinedOutput.files;
if (describedFiles.length === 0) {
const history: AutoBeAssistantMessageHistory = {
id: v4(),
type: "assistantMessage",
text: "The current requirements are insufficient, so file generation will be suspended. It would be better to continue the conversation.",
created_at,
completed_at: new Date().toISOString(),
};
ctx.dispatch({
type: "assistantMessage",
text: "The current requirements are insufficient, so file generation will be suspended. It would be better to continue the conversation.",
created_at,
});
return history;
}
const pointers = await Promise.all(
describedFiles.map(async ({ filename, reason }) => {
const pointer: AutoBeAnalyzePointer = { value: null };
const agent = new AutoBeAnalyzeAgent(
AutoBeAnalyzeReviewer,
ctx,
pointer,
describedFiles.map((el) => el.filename),
);
await agent.conversate(
[
`# Instruction`,
`The names of all the files are as follows: ${describedFiles.join(",")}`,
"Assume that all files are in the same folder. Also, when pointing to the location of a file, go to the relative path.",
"",
`Among the various documents, the part you decided to take care of is as follows.: ${filename}`,
`Only write this document named '${filename}'.`,
"Never write other documents.",
"",
"# User Planning Requirements",
"```md",
JSON.stringify(userPlanningRequirements),
"```",
"The reason why this document needs to be written is as follows.",
`- reason: ${reason}`,
].join("\n"),
);
return pointer;
}),
);
const files = pointers
.map((pointer) => {
return pointer.value?.files ?? {};
})
.reduce((acc, cur) => Object.assign(acc, cur));
if (Object.keys(files).length) {
const history: AutoBeAnalyzeHistory = {
id: v4(),
type: "analyze",
reason: userPlanningRequirements,
prefix,
files: files,
step,
created_at,
completed_at: new Date().toISOString(),
};
ctx.state().analyze = history;
ctx.histories().push(history);
ctx.dispatch({
type: "analyzeComplete",
prefix,
files: files,
step,
created_at,
});
return history;
}
const history: AutoBeAssistantMessageHistory = {
id: v4(),
type: "assistantMessage",
text: determined.find((el) => el.type === "assistantMessage")?.text ?? "",
created_at,
completed_at: new Date().toISOString(),
};
ctx.dispatch({
type: "assistantMessage",
text: determined.find((el) => el.type === "assistantMessage")?.text ?? "",
created_at,
});
return history;
};
export interface IDeterminingInput {
/**
* Prefix for file names and all prisma schema files, table, interface, and
* variable names. For example, if you were to create a bulletin board
* service, the prefix would be bbs. At this time, the name of the document
* would be, for example, 00_bbs_table_of_contents, and bbs would have to be
* attached to the name of all documents. This value would then be passed to
* other agents as well, in the form of bbs_article, bbs_article_snapshot, and
* bbs_comments in the table name. Interfaces will likewise be used in
* interfaces and tests because they originate from the name of prisma scheme.
* Do not use prefixes that are related to the technology stack (e.g., ts_,
* api_, react_) or unnatural prefixes that typically wouldn’t appear in table
* names or domain models (e.g., zz_, my_, dev_).
*
* @title Prefix
*/
prefix: string;
/**
* File name must be English. and it must contains the numbering and prefix.
*
* @title file names and reason to create.
*/
files: Array<Pick<IFile, "filename" | "reason">>;
}
class DeterminingFiles {
/**
* Determining the Initial File List.
*
* Design a list of initial documents that you need to create for that
* requirement. The list of documents is determined only by the name of the
* file. If you determine from the conversation that the user's requirements
* have not been fully gathered, you must stop the analysis and continue
* collecting the remaining requirements. In this case, you do not need to
* generate any files. Simply pass an empty array to `input.files`, which is
* the input value for the `determine` tool.
*
* @param input Prefix and files
* @returns
*/
determine(input: IDeterminingInput): IDeterminingInput {
return input;
}
}
function createController<Model extends ILlmSchema.Model>(props: {
model: Model;
execute: DeterminingFiles;
}): IAgenticaController.IClass<Model> {
assertSchemaModel(props.model);
const application: ILlmApplication<Model> = collection[
props.model
] as unknown as ILlmApplication<Model>;
return {
protocol: "class",
name: "Planning",
application,
// execute: props.execute,
execute: {
determine: (input) => {
return input;
},
} satisfies DeterminingFiles,
};
}
const claude = typia.llm.application<
DeterminingFiles,
"claude",
{ reference: true }
>();
const collection = {
chatgpt: typia.llm.application<
DeterminingFiles,
"chatgpt",
{ reference: true }
>(),
claude,
llama: claude,
deepseek: claude,
"3.1": claude,
"3.0": typia.llm.application<DeterminingFiles, "3.0">(),
};