"Improve personality registry validation and API
This commit is contained in:
+21
-11
@@ -7,14 +7,20 @@ import {asyncHandler} from "../middleware/asyncHandler.js";
|
||||
import {generationCancellation} from "../middleware/generationCancellation.js";
|
||||
import {requireLogin} from "../middleware/auth.js";
|
||||
import {validateBody} from "../middleware/validate.js";
|
||||
import {getPersonality, listPersonalities, DEFAULT_PERSONALITY_ID} from "../services/personalityRegistry.js";
|
||||
import {getPersonality, getPersonalityMetadata, listPersonalities, DEFAULT_PERSONALITY_ID} from "../services/personalityRegistry.js";
|
||||
|
||||
const createChatValidator = (body) => ({
|
||||
value: {
|
||||
title: String(body?.title || "New chat").trim() || "New chat",
|
||||
personalityId: body?.personalityId == null ? DEFAULT_PERSONALITY_ID : String(body.personalityId)
|
||||
const createChatValidator = (body) => {
|
||||
const rawPersonalityId = body?.personalityId;
|
||||
if (rawPersonalityId != null && (typeof rawPersonalityId !== "string" || !rawPersonalityId.trim())) {
|
||||
return {error: "personalityId must be a non-empty string"};
|
||||
}
|
||||
});
|
||||
return {
|
||||
value: {
|
||||
title: String(body?.title || "New chat").trim() || "New chat",
|
||||
personalityId: rawPersonalityId == null ? DEFAULT_PERSONALITY_ID : rawPersonalityId.trim()
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const messageValidator = (body) => {
|
||||
const content = String(body?.content || "").trim();
|
||||
@@ -147,7 +153,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
router.get("/chats", (req, res) => res.json({chats: repositories.listChats(req.session.user)}));
|
||||
router.get("/personalities", (_req, res) => res.json({personalities: listPersonalities(), defaultPersonalityId: DEFAULT_PERSONALITY_ID}));
|
||||
router.get("/personalities/:id", (req, res) => {
|
||||
const personality = listPersonalities().find(item => item.id === req.params.id);
|
||||
const personality = getPersonalityMetadata(req.params.id);
|
||||
return personality ? res.json({personality}) : res.status(404).json({error: "Personality not found"});
|
||||
});
|
||||
router.get("/chats/search", (req, res) => res.json({chats: repositories.searchChats(req.session.user, req.query.q || "")}));
|
||||
@@ -170,10 +176,14 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
const chatId = Number(req.params.id);
|
||||
const chat = repositories.getChat(chatId, req.session.user);
|
||||
if (!chat) return res.status(404).json({error: "Chat not found"});
|
||||
const personalityId = String(req.body?.personalityId || "");
|
||||
if (!getPersonality(personalityId)) return res.status(400).json({error: "Unknown personality"});
|
||||
repositories.updateChatPersonality(chatId, req.session.user, personalityId);
|
||||
return res.json({chat: repositories.getChat(chatId, req.session.user), personality: listPersonalities().find(item => item.id === personalityId)});
|
||||
const personalityId = req.body?.personalityId;
|
||||
if (typeof personalityId !== "string" || !personalityId.trim()) {
|
||||
return res.status(400).json({error: "personalityId must be a non-empty string"});
|
||||
}
|
||||
const normalizedPersonalityId = personalityId.trim();
|
||||
if (!getPersonality(normalizedPersonalityId)) return res.status(400).json({error: "Unknown personality"});
|
||||
repositories.updateChatPersonality(chatId, req.session.user, normalizedPersonalityId);
|
||||
return res.json({chat: repositories.getChat(chatId, req.session.user), personality: getPersonalityMetadata(normalizedPersonalityId)});
|
||||
});
|
||||
|
||||
router.put("/chats/:id", validateBody((body) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const DEFAULT_PERSONALITY_ID = "general-assistant";
|
||||
|
||||
const entries = [
|
||||
{
|
||||
id: "general-assistant", name: "Leonardo", specialization: "General Assistance",
|
||||
@@ -79,15 +81,54 @@ Present the reasoning in a readable order: frame the problem, identify decisive
|
||||
}
|
||||
];
|
||||
|
||||
const registry = new Map(entries.map(entry => [entry.id, Object.freeze(entry)]));
|
||||
export const DEFAULT_PERSONALITY_ID = "general-assistant";
|
||||
const requiredMetadataFields = [
|
||||
"id",
|
||||
"name",
|
||||
"specialization",
|
||||
"description",
|
||||
"longDescription",
|
||||
"historicalInspiration",
|
||||
"icon",
|
||||
"accentColor",
|
||||
"systemPrompt"
|
||||
];
|
||||
|
||||
export function getPersonality(id) {
|
||||
return registry.get(id) || null;
|
||||
function createRegistry(definitions) {
|
||||
const ids = new Set();
|
||||
const defaultEntries = definitions.filter(entry => entry.id === DEFAULT_PERSONALITY_ID);
|
||||
|
||||
if (defaultEntries.length !== 1) {
|
||||
throw new Error(`Personality registry must contain exactly one default personality: ${DEFAULT_PERSONALITY_ID}`);
|
||||
}
|
||||
|
||||
for (const entry of definitions) {
|
||||
if (!entry || typeof entry !== "object") throw new Error("Personality registry entries must be objects");
|
||||
for (const field of requiredMetadataFields) {
|
||||
if (typeof entry[field] !== "string" || !entry[field].trim()) {
|
||||
throw new Error(`Personality ${entry.id || "<unknown>"} is missing ${field}`);
|
||||
}
|
||||
}
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.id)) {
|
||||
throw new Error(`Personality ID is not kebab-case: ${entry.id}`);
|
||||
}
|
||||
if (ids.has(entry.id)) throw new Error(`Duplicate personality ID: ${entry.id}`);
|
||||
if (!/^#[0-9a-f]{6}$/i.test(entry.accentColor)) {
|
||||
throw new Error(`Invalid accent color for personality: ${entry.id}`);
|
||||
}
|
||||
if (entry.systemPrompt.length < 1200) {
|
||||
throw new Error(`Personality ${entry.id} needs a substantial system prompt`);
|
||||
}
|
||||
ids.add(entry.id);
|
||||
}
|
||||
|
||||
return new Map(definitions.map(entry => [entry.id, Object.freeze({...entry})]));
|
||||
}
|
||||
|
||||
export function listPersonalities() {
|
||||
return entries.map(entry => ({
|
||||
const registry = createRegistry(entries);
|
||||
|
||||
function toPublicMetadata(entry) {
|
||||
if (!entry) return null;
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
specialization: entry.specialization,
|
||||
@@ -96,10 +137,26 @@ export function listPersonalities() {
|
||||
historicalInspiration: entry.historicalInspiration,
|
||||
icon: entry.icon,
|
||||
accentColor: entry.accentColor,
|
||||
...(entry.isDefault ? {isDefault: true} : {})
|
||||
}));
|
||||
isDefault: entry.id === DEFAULT_PERSONALITY_ID
|
||||
};
|
||||
}
|
||||
|
||||
export function getPersonality(id) {
|
||||
return registry.get(id) || null;
|
||||
}
|
||||
|
||||
export function resolvePersonalityId(id) {
|
||||
return registry.has(id) ? id : DEFAULT_PERSONALITY_ID;
|
||||
}
|
||||
|
||||
export function listPersonalities() {
|
||||
return entries.map(toPublicMetadata);
|
||||
}
|
||||
|
||||
export function getPersonalityMetadata(id) {
|
||||
return toPublicMetadata(getPersonality(id));
|
||||
}
|
||||
|
||||
export function getPersonalitySystemPrompt(id) {
|
||||
return getPersonality(id)?.systemPrompt || registry.get(DEFAULT_PERSONALITY_ID).systemPrompt;
|
||||
return registry.get(resolvePersonalityId(id)).systemPrompt;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @file Prüft und normalisiert importierte Arbeitsbereiche mit Personas, Chats und Nachrichtenbeziehungen.
|
||||
*/
|
||||
import {parseSources} from "./chatMessageHelpers.js";
|
||||
import {getPersonality, DEFAULT_PERSONALITY_ID} from "./personalityRegistry.js";
|
||||
import {resolvePersonalityId} from "./personalityRegistry.js";
|
||||
|
||||
const VALID_PERSONA_TYPES = new Set(["assistant", "user"]);
|
||||
const VALID_MESSAGE_ROLES = new Set(["user", "bot", "assistant", "system"]);
|
||||
@@ -197,7 +197,7 @@ function validateChat(chat, index, errors) {
|
||||
branched_from_message_id: sourceId(chat.branched_from_message_id ?? chat.branchedFromMessageId),
|
||||
title,
|
||||
stored_title: title,
|
||||
personality_id: getPersonality(chat.personality_id ?? chat.personalityId)?.id || DEFAULT_PERSONALITY_ID,
|
||||
personality_id: resolvePersonalityId(chat.personality_id ?? chat.personalityId),
|
||||
folder_name: optionalText(chat.folder_name || chat.folderName),
|
||||
is_pinned: Boolean(chat.is_pinned || chat.isPinned),
|
||||
archived_at: optionalText(chat.archived_at || chat.archivedAt),
|
||||
|
||||
Reference in New Issue
Block a user