forked from EduCraft/curriculum-project-hub
feat: make agent roles and skills dynamic
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { prisma } from "../db.js";
|
||||
import { OrganizationAgentConfiguration } from "../agent/configuration.js";
|
||||
import { readSkillStoreRoot, verifyStoredSkill } from "../agent/skillStore.js";
|
||||
import { readSiloOrganizationId } from "./silo.js";
|
||||
|
||||
async function main(argv: readonly string[]): Promise<void> {
|
||||
const [command, ...args] = argv;
|
||||
if (command === undefined || command === "help" || command === "--help") {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
const options = parseOptions(args);
|
||||
const organizationId = required(options, "organization");
|
||||
const siloOrganizationId = readSiloOrganizationId();
|
||||
if (organizationId !== siloOrganizationId) {
|
||||
throw new Error(`Silo Agent configuration is restricted to ${siloOrganizationId}`);
|
||||
}
|
||||
const configuration = new OrganizationAgentConfiguration(prisma, readSkillStoreRoot());
|
||||
|
||||
switch (command) {
|
||||
case "install-skill": {
|
||||
const installed = await configuration.installSkill({
|
||||
organizationId,
|
||||
sourceDir: required(options, "source"),
|
||||
version: required(options, "version"),
|
||||
});
|
||||
console.log(JSON.stringify(installed));
|
||||
return;
|
||||
}
|
||||
case "upsert-role": {
|
||||
const systemPromptFile = options.get("system-prompt-file");
|
||||
const toolsJson = options.get("tools-json");
|
||||
const tools = toolsJson === undefined
|
||||
? undefined
|
||||
: toolsJson === "null"
|
||||
? null
|
||||
: parseTools(toolsJson);
|
||||
const sortOrderRaw = options.get("sort-order");
|
||||
const role = await configuration.upsertRole({
|
||||
organizationId,
|
||||
roleId: required(options, "role"),
|
||||
label: required(options, "label"),
|
||||
...(options.has("model") ? { defaultModel: options.get("model") ?? null } : {}),
|
||||
...(systemPromptFile !== undefined
|
||||
? { systemPrompt: await readFile(systemPromptFile, "utf8") }
|
||||
: {}),
|
||||
...(tools !== undefined ? { tools } : {}),
|
||||
...(sortOrderRaw !== undefined ? { sortOrder: integer(sortOrderRaw, "sort-order") } : {}),
|
||||
});
|
||||
console.log(JSON.stringify(role));
|
||||
return;
|
||||
}
|
||||
case "set-role-skills": {
|
||||
const skills = required(options, "skills").split(",").map((name) => name.trim()).filter(Boolean);
|
||||
await configuration.setRoleSkills({
|
||||
organizationId,
|
||||
roleId: required(options, "role"),
|
||||
skillNames: skills,
|
||||
});
|
||||
console.log(JSON.stringify({ roleId: required(options, "role"), skills }));
|
||||
return;
|
||||
}
|
||||
case "list": {
|
||||
const roles = await prisma.organizationAgentRole.findMany({
|
||||
where: { organizationId },
|
||||
orderBy: [{ sortOrder: "asc" }, { roleId: "asc" }],
|
||||
include: {
|
||||
skillBindings: {
|
||||
orderBy: [{ sortOrder: "asc" }, { agentSkillId: "asc" }],
|
||||
include: { skill: { select: { name: true, version: true, disabledAt: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(JSON.stringify(roles.map((role) => ({
|
||||
roleId: role.roleId,
|
||||
label: role.label,
|
||||
defaultModel: role.defaultModel,
|
||||
systemPromptConfigured: role.systemPrompt !== null,
|
||||
tools: role.tools,
|
||||
disabled: role.disabledAt !== null,
|
||||
skills: role.skillBindings.map((binding) => ({
|
||||
name: binding.skill.name,
|
||||
version: binding.skill.version,
|
||||
disabled: binding.skill.disabledAt !== null,
|
||||
})),
|
||||
})), null, 2));
|
||||
return;
|
||||
}
|
||||
case "verify-store": {
|
||||
const skills = await prisma.organizationAgentSkill.findMany({
|
||||
where: { organizationId, disabledAt: null },
|
||||
select: { name: true, contentDigest: true },
|
||||
});
|
||||
for (const skill of skills) {
|
||||
await verifyStoredSkill({ storeRoot: readSkillStoreRoot(), ...skill });
|
||||
}
|
||||
console.log(JSON.stringify({ verifiedSkills: skills.length }));
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error(`unknown Agent configuration command: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseOptions(args: readonly string[]): Map<string, string> {
|
||||
const options = new Map<string, string>();
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const flag = args[index];
|
||||
const value = args[index + 1];
|
||||
if (flag === undefined || !flag.startsWith("--") || value === undefined) {
|
||||
throw new Error(`expected --name value, got: ${args.slice(index).join(" ")}`);
|
||||
}
|
||||
const name = flag.slice(2);
|
||||
if (options.has(name)) throw new Error(`duplicate option: --${name}`);
|
||||
options.set(name, value);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function required(options: ReadonlyMap<string, string>, name: string): string {
|
||||
const value = options.get(name)?.trim();
|
||||
if (value === undefined || value === "") throw new Error(`--${name} is required`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseTools(raw: string): readonly string[] {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
|
||||
throw new Error("--tools-json must be null or a JSON string array");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function integer(raw: string, name: string): number {
|
||||
const value = Number(raw);
|
||||
if (!Number.isSafeInteger(value)) throw new Error(`--${name} must be an integer`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`Usage:
|
||||
agent-config install-skill --organization ORG --source DIR --version VERSION
|
||||
agent-config upsert-role --organization ORG --role ID --label LABEL [--model MODEL] [--system-prompt-file FILE] [--tools-json JSON] [--sort-order N]
|
||||
agent-config set-role-skills --organization ORG --role ID --skills name,name
|
||||
agent-config list --organization ORG
|
||||
agent-config verify-store --organization ORG`);
|
||||
}
|
||||
|
||||
main(process.argv.slice(2))
|
||||
.catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -226,6 +226,22 @@ async function initializeSilo(
|
||||
const count = await tx.organization.count();
|
||||
if (count !== 0) throw new Error(`Silo bootstrap requires an empty Organization set; found ${count}`);
|
||||
await tx.organization.create({ data: { ...input.organization } });
|
||||
await tx.organizationAgentRole.createMany({
|
||||
data: [
|
||||
{
|
||||
organizationId: input.organization.id,
|
||||
roleId: "draft",
|
||||
label: "草稿",
|
||||
sortOrder: 10,
|
||||
},
|
||||
{
|
||||
organizationId: input.organization.id,
|
||||
roleId: "review",
|
||||
label: "审校",
|
||||
sortOrder: 20,
|
||||
},
|
||||
],
|
||||
});
|
||||
await tx.organizationProjectSettings.create({
|
||||
data: { organizationId: input.organization.id, membersCanCreateProjects: true },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user