forked from EduCraft/curriculum-project-hub
feat: make agent roles and skills dynamic
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { InMemoryModelRegistry, type ModelRegistry } from "../agent/models.js";
|
||||
import { InMemoryModelRegistry, type ModelRegistry, type RoleEntry, type RoleSkillEntry } from "../agent/models.js";
|
||||
import { lockActiveOrganization } from "../org/status.js";
|
||||
import { decryptStoredProviderCredential } from "../connections/providerConnections.js";
|
||||
import { openProviderProxyLease, type AgentProviderLease, type ProviderUpstreamCredential } from "../connections/providerProxy.js";
|
||||
@@ -141,8 +141,75 @@ export class DatabaseRuntimeSettings implements RuntimeSettings {
|
||||
};
|
||||
}
|
||||
|
||||
modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry> {
|
||||
return this.envSettings.modelRegistry(scope);
|
||||
async modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry> {
|
||||
const projectId = scope?.projectId?.trim();
|
||||
if (projectId === undefined || projectId === "") {
|
||||
throw new Error("projectId is required to resolve Agent runtime configuration");
|
||||
}
|
||||
const project = await this.prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
select: {
|
||||
archivedAt: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
agentRoles: {
|
||||
where: { disabledAt: null },
|
||||
orderBy: [{ sortOrder: "asc" }, { roleId: "asc" }],
|
||||
include: {
|
||||
skillBindings: {
|
||||
orderBy: [{ sortOrder: "asc" }, { agentSkillId: "asc" }],
|
||||
include: { skill: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (project === null || project.archivedAt !== null) {
|
||||
throw new Error(`active project not found: ${projectId}`);
|
||||
}
|
||||
if (project.organization.status !== "ACTIVE") {
|
||||
throw new Error(`organization ${project.organization.id} is ${project.organization.status}`);
|
||||
}
|
||||
if (project.organization.agentRoles.length === 0) {
|
||||
throw new Error(`no active Agent roles configured for organization ${project.organization.id}`);
|
||||
}
|
||||
|
||||
const defaults = await this.envSettings.modelRegistry(scope);
|
||||
const enabledModels = new Set(defaults.listModels().map((model) => model.id));
|
||||
const roles: RoleEntry[] = project.organization.agentRoles.map((role) => ({
|
||||
id: role.roleId,
|
||||
label: role.label,
|
||||
defaultModel: validateRoleModel(role.roleId, role.defaultModel, enabledModels),
|
||||
...(role.systemPrompt !== null ? { systemPrompt: role.systemPrompt } : {}),
|
||||
...(role.tools !== null ? { tools: roleToolsFromJson(role.roleId, role.tools) } : {}),
|
||||
skills: role.skillBindings.map((binding): RoleSkillEntry => {
|
||||
if (binding.skill.disabledAt !== null) {
|
||||
throw new Error(`role ${role.roleId} selects disabled skill ${binding.skill.name}`);
|
||||
}
|
||||
if (!/^[a-f0-9]{64}$/.test(binding.skill.contentDigest)) {
|
||||
throw new Error(`skill ${binding.skill.name} has invalid content digest`);
|
||||
}
|
||||
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(binding.skill.name)) {
|
||||
throw new Error(`skill has invalid name: ${binding.skill.name}`);
|
||||
}
|
||||
if (binding.skill.version.trim() === "") {
|
||||
throw new Error(`skill ${binding.skill.name} has empty version`);
|
||||
}
|
||||
return {
|
||||
name: binding.skill.name,
|
||||
version: binding.skill.version,
|
||||
contentDigest: binding.skill.contentDigest,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
if (!roles.some((role) => role.id === "draft")) {
|
||||
throw new Error(`default Agent role draft is not configured for organization ${project.organization.id}`);
|
||||
}
|
||||
return new InMemoryModelRegistry(defaults.listModels(), roles);
|
||||
}
|
||||
|
||||
runPolicy(input: RunPolicyInput): Promise<RunPolicy> {
|
||||
@@ -150,6 +217,23 @@ export class DatabaseRuntimeSettings implements RuntimeSettings {
|
||||
}
|
||||
}
|
||||
|
||||
function roleToolsFromJson(roleId: string, value: Prisma.JsonValue): readonly string[] {
|
||||
if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string")) {
|
||||
throw new Error(`role ${roleId} tools must be a JSON string array`);
|
||||
}
|
||||
return value as string[];
|
||||
}
|
||||
|
||||
function validateRoleModel(
|
||||
roleId: string,
|
||||
model: string | null,
|
||||
enabledModels: ReadonlySet<string>,
|
||||
): string | undefined {
|
||||
if (model === null) return undefined;
|
||||
if (!enabledModels.has(model)) throw new Error(`role ${roleId} selects unavailable model ${model}`);
|
||||
return model;
|
||||
}
|
||||
|
||||
async function loadActiveProviderSecret(
|
||||
tx: Prisma.TransactionClient,
|
||||
projectId: string,
|
||||
|
||||
Reference in New Issue
Block a user