"Improve personality registry validation and API
This commit is contained in:
+19
-9
@@ -7,14 +7,20 @@ import {asyncHandler} from "../middleware/asyncHandler.js";
|
|||||||
import {generationCancellation} from "../middleware/generationCancellation.js";
|
import {generationCancellation} from "../middleware/generationCancellation.js";
|
||||||
import {requireLogin} from "../middleware/auth.js";
|
import {requireLogin} from "../middleware/auth.js";
|
||||||
import {validateBody} from "../middleware/validate.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) => ({
|
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: {
|
value: {
|
||||||
title: String(body?.title || "New chat").trim() || "New chat",
|
title: String(body?.title || "New chat").trim() || "New chat",
|
||||||
personalityId: body?.personalityId == null ? DEFAULT_PERSONALITY_ID : String(body.personalityId)
|
personalityId: rawPersonalityId == null ? DEFAULT_PERSONALITY_ID : rawPersonalityId.trim()
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const messageValidator = (body) => {
|
const messageValidator = (body) => {
|
||||||
const content = String(body?.content || "").trim();
|
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("/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", (_req, res) => res.json({personalities: listPersonalities(), defaultPersonalityId: DEFAULT_PERSONALITY_ID}));
|
||||||
router.get("/personalities/:id", (req, res) => {
|
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"});
|
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 || "")}));
|
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 chatId = Number(req.params.id);
|
||||||
const chat = repositories.getChat(chatId, req.session.user);
|
const chat = repositories.getChat(chatId, req.session.user);
|
||||||
if (!chat) return res.status(404).json({error: "Chat not found"});
|
if (!chat) return res.status(404).json({error: "Chat not found"});
|
||||||
const personalityId = String(req.body?.personalityId || "");
|
const personalityId = req.body?.personalityId;
|
||||||
if (!getPersonality(personalityId)) return res.status(400).json({error: "Unknown personality"});
|
if (typeof personalityId !== "string" || !personalityId.trim()) {
|
||||||
repositories.updateChatPersonality(chatId, req.session.user, personalityId);
|
return res.status(400).json({error: "personalityId must be a non-empty string"});
|
||||||
return res.json({chat: repositories.getChat(chatId, req.session.user), personality: listPersonalities().find(item => item.id === personalityId)});
|
}
|
||||||
|
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) => {
|
router.put("/chats/:id", validateBody((body) => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export const DEFAULT_PERSONALITY_ID = "general-assistant";
|
||||||
|
|
||||||
const entries = [
|
const entries = [
|
||||||
{
|
{
|
||||||
id: "general-assistant", name: "Leonardo", specialization: "General Assistance",
|
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)]));
|
const requiredMetadataFields = [
|
||||||
export const DEFAULT_PERSONALITY_ID = "general-assistant";
|
"id",
|
||||||
|
"name",
|
||||||
|
"specialization",
|
||||||
|
"description",
|
||||||
|
"longDescription",
|
||||||
|
"historicalInspiration",
|
||||||
|
"icon",
|
||||||
|
"accentColor",
|
||||||
|
"systemPrompt"
|
||||||
|
];
|
||||||
|
|
||||||
export function getPersonality(id) {
|
function createRegistry(definitions) {
|
||||||
return registry.get(id) || null;
|
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listPersonalities() {
|
for (const entry of definitions) {
|
||||||
return entries.map(entry => ({
|
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})]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = createRegistry(entries);
|
||||||
|
|
||||||
|
function toPublicMetadata(entry) {
|
||||||
|
if (!entry) return null;
|
||||||
|
return {
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
specialization: entry.specialization,
|
specialization: entry.specialization,
|
||||||
@@ -96,10 +137,26 @@ export function listPersonalities() {
|
|||||||
historicalInspiration: entry.historicalInspiration,
|
historicalInspiration: entry.historicalInspiration,
|
||||||
icon: entry.icon,
|
icon: entry.icon,
|
||||||
accentColor: entry.accentColor,
|
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) {
|
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.
|
* @file Prüft und normalisiert importierte Arbeitsbereiche mit Personas, Chats und Nachrichtenbeziehungen.
|
||||||
*/
|
*/
|
||||||
import {parseSources} from "./chatMessageHelpers.js";
|
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_PERSONA_TYPES = new Set(["assistant", "user"]);
|
||||||
const VALID_MESSAGE_ROLES = new Set(["user", "bot", "assistant", "system"]);
|
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),
|
branched_from_message_id: sourceId(chat.branched_from_message_id ?? chat.branchedFromMessageId),
|
||||||
title,
|
title,
|
||||||
stored_title: 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),
|
folder_name: optionalText(chat.folder_name || chat.folderName),
|
||||||
is_pinned: Boolean(chat.is_pinned || chat.isPinned),
|
is_pinned: Boolean(chat.is_pinned || chat.isPinned),
|
||||||
archived_at: optionalText(chat.archived_at || chat.archivedAt),
|
archived_at: optionalText(chat.archived_at || chat.archivedAt),
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ test("personality API keeps prompts private, validates IDs, and enforces chat ow
|
|||||||
const defaultChat = await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{systemPrompt:"Injected"}});
|
const defaultChat = await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{systemPrompt:"Injected"}});
|
||||||
assert.equal(defaultChat.json.chat.personality_id, "general-assistant");
|
assert.equal(defaultChat.json.chat.personality_id, "general-assistant");
|
||||||
assert.equal((await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{personalityId:"invented"}})).status, 400);
|
assert.equal((await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{personalityId:"invented"}})).status, 400);
|
||||||
|
assert.equal((await request(server.baseUrl, "/chats", {method:"POST", jar:owner, body:{personalityId:["programmer"]}})).status, 400);
|
||||||
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:other, body:{personalityId:"tutor"}})).status, 404);
|
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:other, body:{personalityId:"tutor"}})).status, 404);
|
||||||
|
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:owner, body:{personalityId:["tutor"]}})).status, 400);
|
||||||
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:owner, body:{personalityId:"tutor"}})).json.chat.personality_id, "tutor");
|
assert.equal((await request(server.baseUrl, `/chats/${created.json.chat.id}/personality`, {method:"PUT", jar:owner, body:{personalityId:"tutor"}})).json.chat.personality_id, "tutor");
|
||||||
assert.equal((await request(server.baseUrl, "/personas", {method:"POST", jar:owner, body:{name:"Custom"}})).status, 404);
|
assert.equal((await request(server.baseUrl, "/personas", {method:"POST", jar:owner, body:{name:"Custom"}})).status, 404);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user