Overhaul legal page and global styles for consistency and responsiveness.
This commit is contained in:
+78
@@ -1,4 +1,5 @@
|
||||
import fs from "fs";
|
||||
import {randomUUID} from "crypto";
|
||||
import {performance} from "perf_hooks";
|
||||
import express from "express";
|
||||
import session from "express-session";
|
||||
@@ -70,6 +71,78 @@ function createLifecycleMonitors(app, config) {
|
||||
return resources;
|
||||
}
|
||||
|
||||
function safeRequestId(value) {
|
||||
const candidate = String(value || "").trim();
|
||||
if (/^[A-Za-z0-9._:-]{8,128}$/.test(candidate)) return candidate;
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
function createObservability(config) {
|
||||
const counters = {
|
||||
startedAt: new Date().toISOString(),
|
||||
requestsTotal: 0,
|
||||
responsesByStatus: {},
|
||||
errorsTotal: 0,
|
||||
slowRequestsTotal: 0,
|
||||
lastFailure: null
|
||||
};
|
||||
|
||||
const recordError = (req, error, status = 500) => {
|
||||
counters.errorsTotal += 1;
|
||||
counters.lastFailure = {
|
||||
at: new Date().toISOString(),
|
||||
requestId: req?.id || null,
|
||||
method: req?.method || null,
|
||||
path: req?.originalUrl || req?.url || null,
|
||||
status,
|
||||
message: String(error?.message || error || "Unknown error").slice(0, 500)
|
||||
};
|
||||
};
|
||||
|
||||
const middleware = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
req.id = safeRequestId(req.get("X-Request-ID"));
|
||||
res.setHeader("X-Request-ID", req.id);
|
||||
|
||||
res.on("finish", () => {
|
||||
const durationMs = Date.now() - start;
|
||||
const status = res.statusCode;
|
||||
const statusKey = String(status);
|
||||
counters.requestsTotal += 1;
|
||||
counters.responsesByStatus[statusKey] = (counters.responsesByStatus[statusKey] || 0) + 1;
|
||||
if (durationMs > config.slowRequestWarnMs) counters.slowRequestsTotal += 1;
|
||||
if (status >= 500 && counters.lastFailure?.requestId !== req.id) {
|
||||
recordError(req, `HTTP ${status}`, status);
|
||||
}
|
||||
if (config.structuredRequestLoggingEnabled) {
|
||||
console.log(JSON.stringify({
|
||||
type: "request",
|
||||
requestId: req.id,
|
||||
method: req.method,
|
||||
path: req.originalUrl || req.url,
|
||||
status,
|
||||
durationMs,
|
||||
slow: durationMs > config.slowRequestWarnMs,
|
||||
user: req.session?.user || null
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
return {
|
||||
middleware,
|
||||
recordError,
|
||||
getSnapshot() {
|
||||
return {
|
||||
...counters,
|
||||
responsesByStatus: {...counters.responsesByStatus}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function wantsJson(req) {
|
||||
const accept = req.get("Accept") || "";
|
||||
const contentType = req.get("Content-Type") || "";
|
||||
@@ -92,11 +165,13 @@ function errorHandler(err, req, res, next) {
|
||||
}
|
||||
|
||||
if (err?.type === "entity.parse.failed") {
|
||||
req.app?.locals?.observability?.recordError(req, err, 400);
|
||||
return res.status(400).json({error: "Invalid JSON"});
|
||||
}
|
||||
|
||||
const status = Number.isInteger(err.status) ? err.status : Number.isInteger(err.statusCode) ? err.statusCode : 500;
|
||||
const message = status >= 500 ? "Internal server error" : err.message;
|
||||
req.app?.locals?.observability?.recordError(req, err, status);
|
||||
if (status >= 500) {
|
||||
console.warn("REQUEST ERROR", status, req.method, req.url, err);
|
||||
} else {
|
||||
@@ -165,6 +240,7 @@ export function createApp(options = {}) {
|
||||
}
|
||||
|
||||
const closeHandlers = createLifecycleMonitors(app, config);
|
||||
const observability = createObservability(config);
|
||||
|
||||
const db = createDatabase(config);
|
||||
const repositories = createRepositories(db, config);
|
||||
@@ -179,6 +255,7 @@ export function createApp(options = {}) {
|
||||
() => db.close()
|
||||
);
|
||||
|
||||
app.use(observability.middleware);
|
||||
app.use(express.json({limit: "1mb"}));
|
||||
app.use(express.static(config.publicDir, {
|
||||
maxAge: config.staticMaxAge,
|
||||
@@ -204,6 +281,7 @@ export function createApp(options = {}) {
|
||||
app.locals.db = db;
|
||||
app.locals.repositories = repositories;
|
||||
app.locals.modelService = modelService;
|
||||
app.locals.observability = observability;
|
||||
app.locals.close = createCloseHandler(closeHandlers);
|
||||
|
||||
app.use(createAuthRouter({repositories, authRateLimiters, config}));
|
||||
|
||||
@@ -46,6 +46,7 @@ export function createConfig(overrides = {}) {
|
||||
eventLoopLagWarnMs: int(process.env.EVENT_LOOP_LAG_WARN_MS, 50),
|
||||
slowRequestLoggingEnabled: bool(process.env.SLOW_REQUEST_LOGGING_ENABLED, !testMode),
|
||||
slowRequestWarnMs: int(process.env.SLOW_REQUEST_WARN_MS, 50),
|
||||
structuredRequestLoggingEnabled: bool(process.env.STRUCTURED_REQUEST_LOGGING_ENABLED, !testMode),
|
||||
chatLimit: int(process.env.CHAT_LIMIT, 10),
|
||||
chatHistoryLimit: int(process.env.CHAT_HISTORY_LIMIT, 20),
|
||||
chatSummaryUpdateEveryMessages: int(process.env.CHAT_SUMMARY_UPDATE_EVERY_MESSAGES, 8),
|
||||
|
||||
@@ -236,6 +236,50 @@ export function createRepositories(db, config) {
|
||||
AND username = ?
|
||||
`),
|
||||
deleteChatMemory: db.prepare("DELETE FROM chat_memories WHERE id = ? AND chat_id = ? AND username = ?"),
|
||||
getPersonaChatState: db.prepare(`
|
||||
SELECT id,
|
||||
username,
|
||||
chat_id,
|
||||
assistant_persona_id,
|
||||
user_persona_id,
|
||||
relationship_notes,
|
||||
tone,
|
||||
current_location,
|
||||
goals,
|
||||
unresolved_threads,
|
||||
boundaries,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM persona_chat_state
|
||||
WHERE chat_id = ?
|
||||
AND username = ?
|
||||
`),
|
||||
upsertPersonaChatState: db.prepare(`
|
||||
INSERT INTO persona_chat_state (
|
||||
username,
|
||||
chat_id,
|
||||
assistant_persona_id,
|
||||
user_persona_id,
|
||||
relationship_notes,
|
||||
tone,
|
||||
current_location,
|
||||
goals,
|
||||
unresolved_threads,
|
||||
boundaries
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(chat_id) DO UPDATE SET
|
||||
username = excluded.username,
|
||||
assistant_persona_id = excluded.assistant_persona_id,
|
||||
user_persona_id = excluded.user_persona_id,
|
||||
relationship_notes = excluded.relationship_notes,
|
||||
tone = excluded.tone,
|
||||
current_location = excluded.current_location,
|
||||
goals = excluded.goals,
|
||||
unresolved_threads = excluded.unresolved_threads,
|
||||
boundaries = excluded.boundaries,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`),
|
||||
updateChatMessage: db.prepare("UPDATE chat_messages SET content = ? WHERE id = ? AND chat_id = ?"),
|
||||
updateChatMessageWithRetryState: db.prepare("UPDATE chat_messages SET content = ?, retry_variants = ?, retry_active_index = ?, retry_retries_used = ?, retry_prompt_message_id = ? WHERE id = ? AND chat_id = ?"),
|
||||
deleteChatMessage: db.prepare("DELETE FROM chat_messages WHERE id = ? AND chat_id = ?"),
|
||||
@@ -545,6 +589,7 @@ export function createRepositories(db, config) {
|
||||
updatePersonaVersionsUsername: db.prepare("UPDATE persona_versions SET username = ? WHERE username = ?"),
|
||||
updatePromptTemplatesUsername: db.prepare("UPDATE prompt_templates SET username = ? WHERE username = ?"),
|
||||
updateChatMemoriesUsername: db.prepare("UPDATE chat_memories SET username = ? WHERE username = ?"),
|
||||
updatePersonaChatStateUsername: db.prepare("UPDATE persona_chat_state SET username = ? WHERE username = ?"),
|
||||
updateMarketFavoritesUsername: db.prepare("UPDATE persona_market_favorites SET username = ? WHERE username = ?"),
|
||||
updateMarketRatingsUsername: db.prepare("UPDATE persona_market_ratings SET username = ? WHERE username = ?"),
|
||||
updateMarketReportsUsername: db.prepare("UPDATE persona_market_reports SET reporter_username = ? WHERE reporter_username = ?"),
|
||||
@@ -558,6 +603,7 @@ export function createRepositories(db, config) {
|
||||
deleteMarketFavoritesByUser: db.prepare("DELETE FROM persona_market_favorites WHERE username = ?"),
|
||||
deleteMarketRatingsByUser: db.prepare("DELETE FROM persona_market_ratings WHERE username = ?"),
|
||||
deleteMarketReportsByUser: db.prepare("DELETE FROM persona_market_reports WHERE reporter_username = ?"),
|
||||
deletePersonaChatStateByUser: db.prepare("DELETE FROM persona_chat_state WHERE username = ?"),
|
||||
deleteUser: db.prepare("DELETE FROM users WHERE username = ?"),
|
||||
deleteMarketPersonaByPersonaId: db.prepare("DELETE FROM persona_market WHERE persona_id = ? AND creator_username = ?"),
|
||||
incrementMarketUsageCount: db.prepare("UPDATE persona_market SET usage_count = usage_count + 1 WHERE id = ?")
|
||||
@@ -573,6 +619,7 @@ export function createRepositories(db, config) {
|
||||
statements.updatePersonaVersionsUsername.run(nextUsername, currentUsername);
|
||||
statements.updatePromptTemplatesUsername.run(nextUsername, currentUsername);
|
||||
statements.updateChatMemoriesUsername.run(nextUsername, currentUsername);
|
||||
statements.updatePersonaChatStateUsername.run(nextUsername, currentUsername);
|
||||
statements.updateMarketFavoritesUsername.run(nextUsername, currentUsername);
|
||||
statements.updateMarketRatingsUsername.run(nextUsername, currentUsername);
|
||||
statements.updateMarketReportsUsername.run(nextUsername, currentUsername);
|
||||
@@ -586,6 +633,7 @@ export function createRepositories(db, config) {
|
||||
statements.deletePromptTemplatesByUser.run(username);
|
||||
statements.deletePersonaVersionsByUser.run(username);
|
||||
statements.deleteChatMemoriesByUser.run(username);
|
||||
statements.deletePersonaChatStateByUser.run(username);
|
||||
statements.deleteChatSessionsByUser.run(username);
|
||||
statements.deleteUserSettingsByUser.run(username);
|
||||
statements.deletePersonasByUser.run(username);
|
||||
@@ -686,6 +734,21 @@ export function createRepositories(db, config) {
|
||||
if (!fact) continue;
|
||||
statements.insertChatMemory.run(username, chatId, null, null, fact);
|
||||
}
|
||||
const personaState = chat.persona_state || chat.personaChatState;
|
||||
if (personaState) {
|
||||
statements.upsertPersonaChatState.run(
|
||||
username,
|
||||
chatId,
|
||||
null,
|
||||
null,
|
||||
personaState.relationship_notes ?? personaState.relationshipNotes ?? null,
|
||||
personaState.tone ?? null,
|
||||
personaState.current_location ?? personaState.currentLocation ?? null,
|
||||
personaState.goals ?? null,
|
||||
personaState.unresolved_threads ?? personaState.unresolvedThreads ?? null,
|
||||
personaState.boundaries ?? null
|
||||
);
|
||||
}
|
||||
imported.chats += 1;
|
||||
}
|
||||
|
||||
@@ -727,6 +790,22 @@ export function createRepositories(db, config) {
|
||||
updateChatTitle: (chatId, username, title) => statements.updateChatSessionTitle.run(title, chatId, username),
|
||||
updateChatScene: (chatId, username, scenarioPrompt, scenarioSummary) => statements.updateChatSessionScene.run(scenarioPrompt, scenarioSummary, chatId, username),
|
||||
updateChatContextSummary: (chatId, username, summary, messageId) => statements.updateChatContextSummary.run(summary, Number(messageId || 0), chatId, username),
|
||||
getPersonaChatState: (chatId, username) => statements.getPersonaChatState.get(chatId, username) || null,
|
||||
updatePersonaChatState(username, chat, state) {
|
||||
statements.upsertPersonaChatState.run(
|
||||
username,
|
||||
chat.id,
|
||||
chat.assistant_persona_id ?? null,
|
||||
chat.user_persona_id ?? null,
|
||||
state.relationship_notes ?? null,
|
||||
state.tone ?? null,
|
||||
state.current_location ?? null,
|
||||
state.goals ?? null,
|
||||
state.unresolved_threads ?? null,
|
||||
state.boundaries ?? null
|
||||
);
|
||||
return statements.getPersonaChatState.get(chat.id, username);
|
||||
},
|
||||
updateChatOrganization(chatId, username, {folderName = null, isPinned = false, archivedAt = null}) {
|
||||
return statements.updateChatSessionOrganization.run(folderName, isPinned ? 1 : 0, archivedAt, chatId, username);
|
||||
},
|
||||
@@ -994,6 +1073,7 @@ export function createRepositories(db, config) {
|
||||
chats: this.listChats(username).map((chat) => ({
|
||||
...chat,
|
||||
memories: this.listChatMemories(chat.id, username),
|
||||
persona_state: this.getPersonaChatState(chat.id, username),
|
||||
messages: this.listChatMessages(chat.id).map((message) => ({
|
||||
...message,
|
||||
retryVariants: parseJsonArray(message.retry_variants)
|
||||
|
||||
@@ -68,6 +68,29 @@ export function initializeSchema(db) {
|
||||
)
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS persona_chat_state
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
chat_id INTEGER NOT NULL UNIQUE,
|
||||
assistant_persona_id INTEGER,
|
||||
user_persona_id INTEGER,
|
||||
relationship_notes TEXT,
|
||||
tone TEXT,
|
||||
current_location TEXT,
|
||||
goals TEXT,
|
||||
unresolved_threads TEXT,
|
||||
boundaries TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE,
|
||||
FOREIGN KEY (chat_id) REFERENCES chat_sessions (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (assistant_persona_id) REFERENCES personas (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (user_persona_id) REFERENCES personas (id) ON DELETE SET NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS personas
|
||||
(
|
||||
@@ -275,6 +298,29 @@ export function runMigrations(db, {dropLegacyChats = true} = {}) {
|
||||
)
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS persona_chat_state
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
chat_id INTEGER NOT NULL UNIQUE,
|
||||
assistant_persona_id INTEGER,
|
||||
user_persona_id INTEGER,
|
||||
relationship_notes TEXT,
|
||||
tone TEXT,
|
||||
current_location TEXT,
|
||||
goals TEXT,
|
||||
unresolved_threads TEXT,
|
||||
boundaries TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE,
|
||||
FOREIGN KEY (chat_id) REFERENCES chat_sessions (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (assistant_persona_id) REFERENCES personas (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (user_persona_id) REFERENCES personas (id) ON DELETE SET NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS persona_versions
|
||||
(
|
||||
|
||||
@@ -39,6 +39,29 @@ const memoryValidator = (body) => {
|
||||
return {value: {fact}};
|
||||
};
|
||||
|
||||
const personaStateFields = [
|
||||
"relationship_notes",
|
||||
"tone",
|
||||
"current_location",
|
||||
"goals",
|
||||
"unresolved_threads",
|
||||
"boundaries"
|
||||
];
|
||||
|
||||
const normalizeStateText = (value) => String(value ?? "").replace(/\r\n/g, "\n").trim();
|
||||
|
||||
const personaStateValidator = (body) => {
|
||||
const value = {};
|
||||
for (const field of personaStateFields) {
|
||||
const normalized = normalizeStateText(body?.[field]);
|
||||
if (normalized.length > 1200) {
|
||||
return {error: `${field} must be 1200 characters or fewer`};
|
||||
}
|
||||
value[field] = normalized || null;
|
||||
}
|
||||
return {value};
|
||||
};
|
||||
|
||||
export function createChatsRouter({repositories, chatService, modelService, config, chatRateLimiters}) {
|
||||
const router = express.Router();
|
||||
router.use(requireLogin);
|
||||
@@ -247,6 +270,22 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
return res.json({message: "Memory deleted"});
|
||||
});
|
||||
|
||||
router.get("/chats/:id/persona-state", (req, res) => {
|
||||
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"});
|
||||
return res.json({state: repositories.getPersonaChatState(chatId, req.session.user)});
|
||||
});
|
||||
|
||||
router.put("/chats/:id/persona-state", validateBody(personaStateValidator), (req, res) => {
|
||||
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 state = repositories.updatePersonaChatState(req.session.user, chat, req.validatedBody);
|
||||
repositories.touchChat(chatId, req.session.user);
|
||||
return res.json({state});
|
||||
});
|
||||
|
||||
router.post("/chat", ...chatRateLimiters, validateBody(sendMessageValidator), async (req, res) => {
|
||||
try {
|
||||
const result = await chatService.sendChatMessage({
|
||||
|
||||
+13
-73
@@ -7,52 +7,17 @@ import {
|
||||
buildUserPersonaPrompt,
|
||||
containsUserVoiceInRoleplayOpener
|
||||
} from "./personaService.js";
|
||||
import {
|
||||
buildContextSummaryPrompt,
|
||||
buildMemoryPrompt,
|
||||
buildPersonaStatePrompt,
|
||||
clampRetryActiveIndex,
|
||||
formatChatMessage,
|
||||
formatMessagesForSummary,
|
||||
retryStylePrompts
|
||||
} from "./chatMessageHelpers.js";
|
||||
|
||||
export function createChatService(repositories, modelService, config) {
|
||||
const retryStylePrompts = {
|
||||
shorter: "Rewrite the assistant reply to be shorter and tighter while preserving its meaning.",
|
||||
direct: "Rewrite the assistant reply to be more direct and less padded.",
|
||||
emotional: "Rewrite the assistant reply to be more emotionally expressive without changing the core meaning.",
|
||||
"stay-in-character": "Rewrite the assistant reply to stay more strongly in character and voice.",
|
||||
dialogue: "Rewrite the assistant reply as dialogue only. Remove narration unless required for clarity."
|
||||
};
|
||||
|
||||
const clampRetryActiveIndex = (index, variants) => {
|
||||
const numeric = Number(index);
|
||||
if (!Number.isInteger(numeric) || numeric < 0) return 0;
|
||||
return Math.min(numeric, Math.max(0, variants.length - 1));
|
||||
};
|
||||
|
||||
const parseRetryVariants = (rawValue, content) => {
|
||||
if (!rawValue) return [content || ""];
|
||||
try {
|
||||
const parsed = JSON.parse(rawValue);
|
||||
if (Array.isArray(parsed)) {
|
||||
const normalized = parsed.map((item) => String(item || "").trim()).filter(Boolean);
|
||||
if (normalized.length) return normalized;
|
||||
}
|
||||
} catch {
|
||||
return [content || ""];
|
||||
}
|
||||
return [content || ""];
|
||||
};
|
||||
|
||||
const formatChatMessage = (row) => {
|
||||
if (!row) return null;
|
||||
const retryVariants = parseRetryVariants(row.retry_variants, row.content);
|
||||
const retryActiveIndex = clampRetryActiveIndex(row.retry_active_index, retryVariants);
|
||||
return {
|
||||
id: row.id,
|
||||
role: row.role,
|
||||
content: row.content,
|
||||
modelName: row.model_name || null,
|
||||
retryVariants,
|
||||
retryActiveIndex,
|
||||
retryRetriesUsed: Number(row.retry_retries_used || 0),
|
||||
retryPromptMessageId: row.retry_prompt_message_id ?? null
|
||||
};
|
||||
};
|
||||
|
||||
const insertChatMessage = (chatId, role, content, retryState = null, modelName = null) => {
|
||||
const normalizedContent = String(content || "");
|
||||
const payload = retryState
|
||||
@@ -87,41 +52,12 @@ export function createChatService(repositories, modelService, config) {
|
||||
return repositories.getChatMessageByIndex(chatId, safeIndex) || null;
|
||||
};
|
||||
|
||||
const buildMemoryPrompt = (memories) => {
|
||||
const facts = (memories || [])
|
||||
.map((memory) => String(memory.fact || "").trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 30);
|
||||
if (!facts.length) return null;
|
||||
return [
|
||||
"LONG-TERM MEMORY:",
|
||||
"These are durable facts the assistant should remember for this chat/persona context.",
|
||||
"Use them for continuity, but do not mention them unless relevant.",
|
||||
...facts.map((fact, index) => `${index + 1}. ${fact}`)
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
const clampSummary = (value) => {
|
||||
const normalized = String(value || "").trim();
|
||||
if (normalized.length <= config.chatSummaryMaxChars) return normalized;
|
||||
return `${normalized.slice(0, Math.max(0, config.chatSummaryMaxChars - 3)).trimEnd()}...`;
|
||||
};
|
||||
|
||||
const buildContextSummaryPrompt = (summary) => {
|
||||
if (!summary) return null;
|
||||
return [
|
||||
"CONVERSATION SUMMARY:",
|
||||
"This is compressed background context from earlier messages in this chat.",
|
||||
"Use it for continuity. Recent raw messages below are more authoritative.",
|
||||
summary
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
const formatMessagesForSummary = (messages) => messages.map((message) => {
|
||||
const speaker = message.role === "bot" ? "Assistant" : message.role === "user" ? "User" : message.role;
|
||||
return `${speaker}: ${String(message.content || "").trim()}`;
|
||||
}).join("\n\n");
|
||||
|
||||
const maybeUpdateContextSummary = async ({user, chatId, selectedModel}) => {
|
||||
if (config.chatSummaryUpdateEveryMessages <= 0) return;
|
||||
const session = repositories.getChat(chatId, user);
|
||||
@@ -233,6 +169,7 @@ export function createChatService(repositories, modelService, config) {
|
||||
const activeUserPersona = repositories.getUserPersonaForChat(chatId, user) || repositories.getActiveUserPersona(user);
|
||||
const promptMessage = repositories.getPreviousUserMessage(chatId, targetMessage.id);
|
||||
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
|
||||
const personaStatePrompt = buildPersonaStatePrompt(repositories.getPersonaChatState(chatId, user));
|
||||
|
||||
if (!promptMessage) {
|
||||
if (!activePersona || !session.scenario_summary) {
|
||||
@@ -269,6 +206,7 @@ export function createChatService(repositories, modelService, config) {
|
||||
const contextSummaryPrompt = buildContextSummaryPrompt(session.context_summary);
|
||||
if (contextSummaryPrompt) messagesPayload.push({role: "system", content: contextSummaryPrompt});
|
||||
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
|
||||
if (personaStatePrompt) messagesPayload.push({role: "system", content: personaStatePrompt});
|
||||
messagesPayload.push(...conversation.map((message) => ({
|
||||
role: message.role === "bot" ? "assistant" : "user",
|
||||
content: message.content
|
||||
@@ -318,6 +256,8 @@ export function createChatService(repositories, modelService, config) {
|
||||
if (contextSummaryPrompt) messagesPayload.push({role: "system", content: contextSummaryPrompt});
|
||||
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
|
||||
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
|
||||
const personaStatePrompt = buildPersonaStatePrompt(repositories.getPersonaChatState(chatId, user));
|
||||
if (personaStatePrompt) messagesPayload.push({role: "system", content: personaStatePrompt});
|
||||
messagesPayload.push(...conversation.map((entry) => ({
|
||||
role: entry.role === "bot" ? "assistant" : "user",
|
||||
content: entry.content
|
||||
|
||||
@@ -104,6 +104,22 @@ function validateMemory(memory, chatIndex, memoryIndex, errors) {
|
||||
return {fact};
|
||||
}
|
||||
|
||||
function validatePersonaState(state, chatIndex, errors) {
|
||||
if (state == null) return null;
|
||||
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
||||
errors.push(`Chat ${chatIndex + 1}, persona_state must be an object.`);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
relationship_notes: optionalText(state.relationship_notes || state.relationshipNotes),
|
||||
tone: optionalText(state.tone),
|
||||
current_location: optionalText(state.current_location || state.currentLocation),
|
||||
goals: optionalText(state.goals),
|
||||
unresolved_threads: optionalText(state.unresolved_threads || state.unresolvedThreads),
|
||||
boundaries: optionalText(state.boundaries)
|
||||
};
|
||||
}
|
||||
|
||||
function validateChat(chat, index, errors) {
|
||||
if (!chat || typeof chat !== "object" || Array.isArray(chat)) {
|
||||
errors.push(`Chat ${index + 1} must be an object.`);
|
||||
@@ -112,6 +128,7 @@ function validateChat(chat, index, errors) {
|
||||
const title = normalizeText(chat.stored_title || chat.title || "Imported chat") || "Imported chat";
|
||||
const messages = asArray(chat.messages).map((message, messageIndex) => validateMessage(message, index, messageIndex, errors)).filter(Boolean);
|
||||
const memories = asArray(chat.memories).map((memory, memoryIndex) => validateMemory(memory, index, memoryIndex, errors)).filter(Boolean);
|
||||
const personaState = validatePersonaState(chat.persona_state || chat.personaChatState, index, errors);
|
||||
return {
|
||||
title,
|
||||
stored_title: title,
|
||||
@@ -125,7 +142,8 @@ function validateChat(chat, index, errors) {
|
||||
? Number(chat.context_summary_message_id || chat.contextSummaryMessageId)
|
||||
: 0,
|
||||
messages,
|
||||
memories
|
||||
memories,
|
||||
persona_state: personaState
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user