Add per-chat settings UI in web app for additional system prompt and tool checkboxes

This commit is contained in:
Agent
2026-05-24 22:04:05 +00:00
committed by James Magahern
parent 0bf0f95a67
commit 4a2493c421
11 changed files with 321 additions and 32 deletions

View File

@@ -8,6 +8,7 @@ import { env } from "./env.js";
import { buildComparableAttachments } from "./llm/message-content.js";
import { runMultiplex } from "./llm/multiplexer.js";
import { runMultiplexStream, type StreamEvent } from "./llm/streaming.js";
import { getAvailableChatTools, normalizeEnabledChatTools } from "./llm/chat-tools.js";
import { getModelCatalogSnapshot } from "./llm/model-catalog.js";
import { openaiClient } from "./llm/providers.js";
import { serializeProviderFields, toPrismaProvider } from "./llm/provider-ids.js";
@@ -16,6 +17,8 @@ import { isFreshSearchCacheHit, normalizeSearchQuery } from "./search-cache.js";
import type { ChatAttachment } from "./llm/types.js";
const ProviderSchema = z.enum(["openai", "anthropic", "xai", "hermes-agent"]);
const MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS = 12_000;
const EnabledToolsSchema = z.array(z.string().trim().min(1).max(80)).max(20).transform((value) => normalizeEnabledChatTools(value));
type IncomingChatMessage = {
role: "system" | "user" | "assistant" | "tool";
@@ -169,6 +172,8 @@ const CompletionStreamBody = z
provider: ProviderSchema,
model: z.string().min(1),
messages: z.array(CompletionMessageSchema),
additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).optional(),
enabledTools: EnabledToolsSchema.optional(),
userLocation: z.string().trim().min(1).max(200).optional(),
temperature: z.number().min(0).max(2).optional(),
maxTokens: z.number().int().positive().optional(),
@@ -194,6 +199,41 @@ function mergeAttachmentsIntoMetadata(metadata: unknown, attachments?: ChatAttac
};
}
function normalizeAdditionalSystemPrompt(value: string | null | undefined) {
const trimmed = value?.trim();
return trimmed || null;
}
function prependAdditionalSystemPrompt<T extends { messages: IncomingChatMessage[]; additionalSystemPrompt?: string | null }>(body: T): T {
const additionalSystemPrompt = normalizeAdditionalSystemPrompt(body.additionalSystemPrompt);
if (!additionalSystemPrompt) return { ...body, additionalSystemPrompt: undefined };
return {
...body,
additionalSystemPrompt,
messages: [{ role: "system", content: additionalSystemPrompt }, ...body.messages],
};
}
async function applyStoredChatSettings<T extends { chatId?: string; messages: IncomingChatMessage[]; additionalSystemPrompt?: string; enabledTools?: string[] }>(
body: T
) {
if (!body.chatId || (body.additionalSystemPrompt !== undefined && body.enabledTools !== undefined)) {
return prependAdditionalSystemPrompt(body);
}
const chat = await prisma.chat.findUnique({
where: { id: body.chatId },
select: { additionalSystemPrompt: true, enabledTools: true },
});
if (!chat) return prependAdditionalSystemPrompt(body);
return prependAdditionalSystemPrompt({
...body,
additionalSystemPrompt: body.additionalSystemPrompt ?? chat.additionalSystemPrompt ?? undefined,
enabledTools: body.enabledTools ?? normalizeEnabledChatTools(chat.enabledTools),
});
}
const SearchRunBody = z.object({
query: z.string().trim().min(1).optional(),
title: z.string().trim().min(1).optional(),
@@ -377,6 +417,8 @@ const chatSummarySelect = {
initiatedModel: true,
lastUsedProvider: true,
lastUsedModel: true,
additionalSystemPrompt: true,
enabledTools: true,
projectItems: starredProjectItemsSelect,
} as const;
@@ -754,6 +796,11 @@ export async function registerRoutes(app: FastifyInstance) {
return { providers: getModelCatalogSnapshot() };
});
app.get("/v1/chat-tools", async (req) => {
requireAdmin(req);
return { tools: getAvailableChatTools() };
});
app.get("/v1/active-runs", async (req) => {
requireAdmin(req);
return {
@@ -784,6 +831,8 @@ export async function registerRoutes(app: FastifyInstance) {
title: z.string().optional(),
provider: ProviderSchema.optional(),
model: z.string().trim().min(1).optional(),
additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).optional(),
enabledTools: EnabledToolsSchema.optional(),
messages: z.array(CompletionMessageSchema).optional(),
})
.superRefine((value, ctx) => {
@@ -812,6 +861,8 @@ export async function registerRoutes(app: FastifyInstance) {
initiatedModel: body.model,
lastUsedProvider: body.provider ? (toPrismaProvider(body.provider) as any) : undefined,
lastUsedModel: body.model,
additionalSystemPrompt: normalizeAdditionalSystemPrompt(body.additionalSystemPrompt),
enabledTools: body.enabledTools as any,
messages: body.messages?.length
? {
create: body.messages.map((message) => ({
@@ -831,13 +882,22 @@ export async function registerRoutes(app: FastifyInstance) {
app.patch("/v1/chats/:chatId", async (req) => {
requireAdmin(req);
const Params = z.object({ chatId: z.string() });
const Body = z.object({ title: z.string().trim().min(1) });
const Body = z.object({
title: z.string().trim().min(1).optional(),
additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).nullable().optional(),
enabledTools: EnabledToolsSchema.optional(),
});
const { chatId } = Params.parse(req.params);
const body = Body.parse(req.body ?? {});
const data: Record<string, unknown> = {};
if (body.title !== undefined) data.title = body.title;
if (body.additionalSystemPrompt !== undefined) data.additionalSystemPrompt = normalizeAdditionalSystemPrompt(body.additionalSystemPrompt);
if (body.enabledTools !== undefined) data.enabledTools = body.enabledTools;
const updated = await prisma.chat.updateMany({
where: { id: chatId },
data: { title: body.title },
data: data as any,
});
if (updated.count === 0) return app.httpErrors.notFound("chat not found");
@@ -1249,6 +1309,8 @@ export async function registerRoutes(app: FastifyInstance) {
provider: ProviderSchema,
model: z.string().min(1),
messages: z.array(CompletionMessageSchema),
additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).optional(),
enabledTools: EnabledToolsSchema.optional(),
userLocation: z.string().trim().min(1).max(200).optional(),
temperature: z.number().min(0).max(2).optional(),
maxTokens: z.number().int().positive().optional(),
@@ -1269,7 +1331,7 @@ export async function registerRoutes(app: FastifyInstance) {
await storeNonAssistantMessages(body.chatId, body.messages);
}
const result = await runMultiplex(body);
const result = await runMultiplex(await applyStoredChatSettings(body));
return {
chatId: body.chatId ?? null,
@@ -1300,14 +1362,14 @@ export async function registerRoutes(app: FastifyInstance) {
if (activeChatStreams.has(body.chatId)) {
return app.httpErrors.conflict("chat completion already running");
}
const stream = startActiveChatStream(body.chatId, body);
const stream = startActiveChatStream(body.chatId, await applyStoredChatSettings(body));
return streamActiveRun(req, reply, stream);
}
reply.raw.writeHead(200, buildSseHeaders(typeof req.headers.origin === "string" ? req.headers.origin : undefined));
reply.raw.flushHeaders();
for await (const ev of runMultiplexStream(body)) {
for await (const ev of runMultiplexStream(await applyStoredChatSettings(body))) {
writeSseEvent(reply, mapChatStreamEvent(ev));
}