Add per-chat settings UI in web app for additional system prompt and tool checkboxes
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Chat" ADD COLUMN "additionalSystemPrompt" TEXT;
|
||||||
|
ALTER TABLE "Chat" ADD COLUMN "enabledTools" JSONB;
|
||||||
@@ -51,6 +51,9 @@ model Chat {
|
|||||||
lastUsedProvider Provider?
|
lastUsedProvider Provider?
|
||||||
lastUsedModel String?
|
lastUsedModel String?
|
||||||
|
|
||||||
|
additionalSystemPrompt String?
|
||||||
|
enabledTools Json?
|
||||||
|
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
userId String?
|
userId String?
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,43 @@ const CHAT_TOOLS: any[] = [
|
|||||||
...(env.CHAT_SHELL_TOOL_ENABLED ? [SHELL_EXEC_TOOL] : []),
|
...(env.CHAT_SHELL_TOOL_ENABLED ? [SHELL_EXEC_TOOL] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const RESPONSES_CHAT_TOOLS: any[] = CHAT_TOOLS.map((tool) => {
|
function getToolName(tool: any) {
|
||||||
|
return typeof tool?.function?.name === "string" ? tool.function.name : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvailableChatTools() {
|
||||||
|
return CHAT_TOOLS.map((tool) => {
|
||||||
|
const name = getToolName(tool);
|
||||||
|
if (!name) return null;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description: typeof tool?.function?.description === "string" ? tool.function.description : "",
|
||||||
|
};
|
||||||
|
}).filter((tool): tool is { name: string; description: string } => tool !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeEnabledChatTools(value: unknown) {
|
||||||
|
if (!Array.isArray(value)) return getAvailableChatTools().map((tool) => tool.name);
|
||||||
|
const available = new Set(getAvailableChatTools().map((tool) => tool.name));
|
||||||
|
return [...new Set(value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean))].filter((name) =>
|
||||||
|
available.has(name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledToolSet(params: Pick<ToolAwareCompletionParams, "enabledTools">) {
|
||||||
|
return new Set(normalizeEnabledChatTools(params.enabledTools));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledChatTools(params: Pick<ToolAwareCompletionParams, "enabledTools">) {
|
||||||
|
const enabled = getEnabledToolSet(params);
|
||||||
|
return CHAT_TOOLS.filter((tool) => {
|
||||||
|
const name = getToolName(tool);
|
||||||
|
return name ? enabled.has(name) : false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toResponsesChatTools(tools: any[]) {
|
||||||
|
return tools.map((tool) => {
|
||||||
if (tool?.type !== "function") return tool;
|
if (tool?.type !== "function") return tool;
|
||||||
return {
|
return {
|
||||||
type: "function",
|
type: "function",
|
||||||
@@ -201,7 +237,8 @@ const RESPONSES_CHAT_TOOLS: any[] = CHAT_TOOLS.map((tool) => {
|
|||||||
parameters: tool.function.parameters,
|
parameters: tool.function.parameters,
|
||||||
strict: false,
|
strict: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const CHAT_TOOL_SYSTEM_PROMPT =
|
export const CHAT_TOOL_SYSTEM_PROMPT =
|
||||||
"You can use tools to gather up-to-date web information when needed. " +
|
"You can use tools to gather up-to-date web information when needed. " +
|
||||||
@@ -243,6 +280,7 @@ type ToolAwareCompletionParams = {
|
|||||||
client: OpenAI;
|
client: OpenAI;
|
||||||
model: string;
|
model: string;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
|
enabledTools?: string[];
|
||||||
userLocation?: string;
|
userLocation?: string;
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
@@ -384,20 +422,38 @@ function extractHtmlTitle(html: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeIncomingMessages(messages: ChatMessage[], userLocation?: string) {
|
function buildChatToolSystemPrompt(params: Pick<ToolAwareCompletionParams, "enabledTools">) {
|
||||||
|
const enabled = getEnabledToolSet(params);
|
||||||
|
return (
|
||||||
|
"You can use tools to gather up-to-date web information when needed. " +
|
||||||
|
(enabled.has("web_search") ? "Use web_search for discovery and recent facts. " : "") +
|
||||||
|
(enabled.has("fetch_url") ? "Use fetch_url to read the full content of a specific page. " : "") +
|
||||||
|
"Prefer tools when the user asks for current events, verification, sources, or details you do not already have. " +
|
||||||
|
"When you decide tool use is needed, call the tool immediately in the same response; do not say you are running a tool unless you actually call it. " +
|
||||||
|
(enabled.has("codex_exec")
|
||||||
|
? "Use codex_exec when a request needs substantial coding work, repository inspection, shell commands, tests, debugging, or another complex task suited to a persistent Codex workspace. Provide codex_exec a complete prompt with the goal, constraints, assumptions, and expected report-back format. Never ask codex_exec to wait for user input or run interactive commands. "
|
||||||
|
: "") +
|
||||||
|
(enabled.has("shell_exec")
|
||||||
|
? "Use shell_exec for direct non-interactive command-line work on the remote devbox, including quick Python programs, calculations, file inspection, running tests, and small scripts. "
|
||||||
|
: "") +
|
||||||
|
"Do not fabricate tool outputs; reason only from provided tool results."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeIncomingMessages(messages: ChatMessage[], userLocation?: string, params: Pick<ToolAwareCompletionParams, "enabledTools"> = {}) {
|
||||||
const normalized = messages.map((message) => buildOpenAIConversationMessage(message));
|
const normalized = messages.map((message) => buildOpenAIConversationMessage(message));
|
||||||
|
|
||||||
return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, buildSystemPromptAugmentationMessage(userLocation), ...normalized];
|
return [{ role: "system", content: buildChatToolSystemPrompt(params) }, buildSystemPromptAugmentationMessage(userLocation), ...normalized];
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePlainIncomingMessages(messages: ChatMessage[], userLocation?: string) {
|
function normalizePlainIncomingMessages(messages: ChatMessage[], userLocation?: string) {
|
||||||
return [buildSystemPromptAugmentationMessage(userLocation), ...messages.map((message) => buildOpenAIConversationMessage(message))];
|
return [buildSystemPromptAugmentationMessage(userLocation), ...messages.map((message) => buildOpenAIConversationMessage(message))];
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeIncomingResponsesInput(messages: ChatMessage[], userLocation?: string) {
|
function normalizeIncomingResponsesInput(messages: ChatMessage[], userLocation?: string, params: Pick<ToolAwareCompletionParams, "enabledTools"> = {}) {
|
||||||
const normalized = messages.map((message) => buildOpenAIResponsesInputMessage(message));
|
const normalized = messages.map((message) => buildOpenAIResponsesInputMessage(message));
|
||||||
|
|
||||||
return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, buildSystemPromptAugmentationMessage(userLocation), ...normalized];
|
return [{ role: "system", content: buildChatToolSystemPrompt(params) }, buildSystemPromptAugmentationMessage(userLocation), ...normalized];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runExaWebSearchTool(args: WebSearchArgs): Promise<ToolRunOutcome> {
|
async function runExaWebSearchTool(args: WebSearchArgs): Promise<ToolRunOutcome> {
|
||||||
@@ -962,7 +1018,8 @@ async function executeToolCallAndBuildEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> {
|
export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> {
|
||||||
const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation);
|
const enabledTools = getEnabledChatTools(params);
|
||||||
|
const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation, params);
|
||||||
const rawResponses: unknown[] = [];
|
const rawResponses: unknown[] = [];
|
||||||
const toolEvents: ToolExecutionEvent[] = [];
|
const toolEvents: ToolExecutionEvent[] = [];
|
||||||
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
||||||
@@ -976,7 +1033,7 @@ export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams):
|
|||||||
input,
|
input,
|
||||||
temperature: params.temperature,
|
temperature: params.temperature,
|
||||||
max_output_tokens: params.maxTokens,
|
max_output_tokens: params.maxTokens,
|
||||||
tools: RESPONSES_CHAT_TOOLS,
|
tools: toResponsesChatTools(enabledTools),
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
// Tool loops pass response output items back as input; reasoning items need persistence.
|
// Tool loops pass response output items back as input; reasoning items need persistence.
|
||||||
@@ -1031,7 +1088,8 @@ export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runToolAwareChatCompletions(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> {
|
export async function runToolAwareChatCompletions(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> {
|
||||||
const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation);
|
const enabledTools = getEnabledChatTools(params);
|
||||||
|
const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation, params);
|
||||||
const rawResponses: unknown[] = [];
|
const rawResponses: unknown[] = [];
|
||||||
const toolEvents: ToolExecutionEvent[] = [];
|
const toolEvents: ToolExecutionEvent[] = [];
|
||||||
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
||||||
@@ -1045,7 +1103,7 @@ export async function runToolAwareChatCompletions(params: ToolAwareCompletionPar
|
|||||||
messages: conversation,
|
messages: conversation,
|
||||||
temperature: params.temperature,
|
temperature: params.temperature,
|
||||||
max_tokens: params.maxTokens,
|
max_tokens: params.maxTokens,
|
||||||
tools: CHAT_TOOLS,
|
tools: enabledTools,
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
} as any);
|
} as any);
|
||||||
rawResponses.push(completion);
|
rawResponses.push(completion);
|
||||||
@@ -1139,7 +1197,8 @@ export async function runPlainChatCompletions(params: ToolAwareCompletionParams)
|
|||||||
export async function* runToolAwareOpenAIChatStream(
|
export async function* runToolAwareOpenAIChatStream(
|
||||||
params: ToolAwareCompletionParams
|
params: ToolAwareCompletionParams
|
||||||
): AsyncGenerator<ToolAwareStreamingEvent> {
|
): AsyncGenerator<ToolAwareStreamingEvent> {
|
||||||
const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation);
|
const enabledTools = getEnabledChatTools(params);
|
||||||
|
const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation, params);
|
||||||
const rawResponses: unknown[] = [];
|
const rawResponses: unknown[] = [];
|
||||||
const toolEvents: ToolExecutionEvent[] = [];
|
const toolEvents: ToolExecutionEvent[] = [];
|
||||||
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
||||||
@@ -1153,7 +1212,7 @@ export async function* runToolAwareOpenAIChatStream(
|
|||||||
input,
|
input,
|
||||||
temperature: params.temperature,
|
temperature: params.temperature,
|
||||||
max_output_tokens: params.maxTokens,
|
max_output_tokens: params.maxTokens,
|
||||||
tools: RESPONSES_CHAT_TOOLS,
|
tools: toResponsesChatTools(enabledTools),
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
// Tool loops pass response output items back as input; reasoning items need persistence.
|
// Tool loops pass response output items back as input; reasoning items need persistence.
|
||||||
@@ -1265,7 +1324,8 @@ export async function* runToolAwareOpenAIChatStream(
|
|||||||
export async function* runToolAwareChatCompletionsStream(
|
export async function* runToolAwareChatCompletionsStream(
|
||||||
params: ToolAwareCompletionParams
|
params: ToolAwareCompletionParams
|
||||||
): AsyncGenerator<ToolAwareStreamingEvent> {
|
): AsyncGenerator<ToolAwareStreamingEvent> {
|
||||||
const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation);
|
const enabledTools = getEnabledChatTools(params);
|
||||||
|
const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation, params);
|
||||||
const rawResponses: unknown[] = [];
|
const rawResponses: unknown[] = [];
|
||||||
const toolEvents: ToolExecutionEvent[] = [];
|
const toolEvents: ToolExecutionEvent[] = [];
|
||||||
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
||||||
@@ -1279,7 +1339,7 @@ export async function* runToolAwareChatCompletionsStream(
|
|||||||
messages: conversation,
|
messages: conversation,
|
||||||
temperature: params.temperature,
|
temperature: params.temperature,
|
||||||
max_tokens: params.maxTokens,
|
max_tokens: params.maxTokens,
|
||||||
tools: CHAT_TOOLS,
|
tools: enabledTools,
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
stream: true,
|
stream: true,
|
||||||
stream_options: { include_usage: true },
|
stream_options: { include_usage: true },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { performance } from "node:perf_hooks";
|
import { performance } from "node:perf_hooks";
|
||||||
import { prisma } from "../db.js";
|
import { prisma } from "../db.js";
|
||||||
import { anthropicClient, hermesAgentClient, openaiClient, xaiClient } from "./providers.js";
|
import { anthropicClient, hermesAgentClient, openaiClient, xaiClient } from "./providers.js";
|
||||||
import { buildToolLogMessageData, runPlainChatCompletions, runToolAwareChatCompletions, runToolAwareOpenAIChat } from "./chat-tools.js";
|
import { buildToolLogMessageData, normalizeEnabledChatTools, runPlainChatCompletions, runToolAwareChatCompletions, runToolAwareOpenAIChat } from "./chat-tools.js";
|
||||||
import { buildAnthropicConversationMessage, getAnthropicSystemPrompt } from "./message-content.js";
|
import { buildAnthropicConversationMessage, getAnthropicSystemPrompt } from "./message-content.js";
|
||||||
import { toPrismaProvider } from "./provider-ids.js";
|
import { toPrismaProvider } from "./provider-ids.js";
|
||||||
import type { MultiplexRequest, MultiplexResponse, Provider } from "./types.js";
|
import type { MultiplexRequest, MultiplexResponse, Provider } from "./types.js";
|
||||||
@@ -47,13 +47,15 @@ export async function runMultiplex(req: MultiplexRequest): Promise<MultiplexResp
|
|||||||
let usage: MultiplexResponse["usage"] | undefined;
|
let usage: MultiplexResponse["usage"] | undefined;
|
||||||
let raw: unknown;
|
let raw: unknown;
|
||||||
let toolMessages: ReturnType<typeof buildToolLogMessageData>[] = [];
|
let toolMessages: ReturnType<typeof buildToolLogMessageData>[] = [];
|
||||||
|
const enabledTools = normalizeEnabledChatTools(req.enabledTools);
|
||||||
|
|
||||||
if (req.provider === "openai") {
|
if (req.provider === "openai" && enabledTools.length > 0) {
|
||||||
const client = openaiClient();
|
const client = openaiClient();
|
||||||
const r = await runToolAwareOpenAIChat({
|
const r = await runToolAwareOpenAIChat({
|
||||||
client,
|
client,
|
||||||
model: req.model,
|
model: req.model,
|
||||||
messages: req.messages,
|
messages: req.messages,
|
||||||
|
enabledTools,
|
||||||
userLocation: req.userLocation,
|
userLocation: req.userLocation,
|
||||||
temperature: req.temperature,
|
temperature: req.temperature,
|
||||||
maxTokens: req.maxTokens,
|
maxTokens: req.maxTokens,
|
||||||
@@ -67,12 +69,13 @@ export async function runMultiplex(req: MultiplexRequest): Promise<MultiplexResp
|
|||||||
outText = r.text;
|
outText = r.text;
|
||||||
usage = r.usage;
|
usage = r.usage;
|
||||||
toolMessages = r.toolEvents.map((event) => buildToolLogMessageData(call.chatId, event));
|
toolMessages = r.toolEvents.map((event) => buildToolLogMessageData(call.chatId, event));
|
||||||
} else if (req.provider === "xai") {
|
} else if (req.provider === "xai" && enabledTools.length > 0) {
|
||||||
const client = xaiClient();
|
const client = xaiClient();
|
||||||
const r = await runToolAwareChatCompletions({
|
const r = await runToolAwareChatCompletions({
|
||||||
client,
|
client,
|
||||||
model: req.model,
|
model: req.model,
|
||||||
messages: req.messages,
|
messages: req.messages,
|
||||||
|
enabledTools,
|
||||||
userLocation: req.userLocation,
|
userLocation: req.userLocation,
|
||||||
temperature: req.temperature,
|
temperature: req.temperature,
|
||||||
maxTokens: req.maxTokens,
|
maxTokens: req.maxTokens,
|
||||||
@@ -86,8 +89,8 @@ export async function runMultiplex(req: MultiplexRequest): Promise<MultiplexResp
|
|||||||
outText = r.text;
|
outText = r.text;
|
||||||
usage = r.usage;
|
usage = r.usage;
|
||||||
toolMessages = r.toolEvents.map((event) => buildToolLogMessageData(call.chatId, event));
|
toolMessages = r.toolEvents.map((event) => buildToolLogMessageData(call.chatId, event));
|
||||||
} else if (req.provider === "hermes-agent") {
|
} else if (req.provider === "openai" || req.provider === "xai" || req.provider === "hermes-agent") {
|
||||||
const client = hermesAgentClient();
|
const client = req.provider === "openai" ? openaiClient() : req.provider === "xai" ? xaiClient() : hermesAgentClient();
|
||||||
const r = await runPlainChatCompletions({
|
const r = await runPlainChatCompletions({
|
||||||
client,
|
client,
|
||||||
model: req.model,
|
model: req.model,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { prisma } from "../db.js";
|
|||||||
import { anthropicClient, hermesAgentClient, openaiClient, xaiClient } from "./providers.js";
|
import { anthropicClient, hermesAgentClient, openaiClient, xaiClient } from "./providers.js";
|
||||||
import {
|
import {
|
||||||
buildToolLogMessageData,
|
buildToolLogMessageData,
|
||||||
|
normalizeEnabledChatTools,
|
||||||
runPlainChatCompletionsStream,
|
runPlainChatCompletionsStream,
|
||||||
runToolAwareChatCompletionsStream,
|
runToolAwareChatCompletionsStream,
|
||||||
runToolAwareOpenAIChatStream,
|
runToolAwareOpenAIChatStream,
|
||||||
@@ -76,12 +77,14 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator
|
|||||||
try {
|
try {
|
||||||
if (req.provider === "openai" || req.provider === "xai" || req.provider === "hermes-agent") {
|
if (req.provider === "openai" || req.provider === "xai" || req.provider === "hermes-agent") {
|
||||||
const client = req.provider === "openai" ? openaiClient() : req.provider === "xai" ? xaiClient() : hermesAgentClient();
|
const client = req.provider === "openai" ? openaiClient() : req.provider === "xai" ? xaiClient() : hermesAgentClient();
|
||||||
|
const enabledTools = normalizeEnabledChatTools(req.enabledTools);
|
||||||
const streamEvents =
|
const streamEvents =
|
||||||
req.provider === "openai"
|
req.provider === "openai" && enabledTools.length > 0
|
||||||
? runToolAwareOpenAIChatStream({
|
? runToolAwareOpenAIChatStream({
|
||||||
client,
|
client,
|
||||||
model: req.model,
|
model: req.model,
|
||||||
messages: req.messages,
|
messages: req.messages,
|
||||||
|
enabledTools,
|
||||||
userLocation: req.userLocation,
|
userLocation: req.userLocation,
|
||||||
temperature: req.temperature,
|
temperature: req.temperature,
|
||||||
maxTokens: req.maxTokens,
|
maxTokens: req.maxTokens,
|
||||||
@@ -91,7 +94,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator
|
|||||||
chatId: chatId ?? undefined,
|
chatId: chatId ?? undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: req.provider === "hermes-agent"
|
: req.provider === "hermes-agent" || enabledTools.length === 0
|
||||||
? runPlainChatCompletionsStream({
|
? runPlainChatCompletionsStream({
|
||||||
client,
|
client,
|
||||||
model: req.model,
|
model: req.model,
|
||||||
@@ -109,6 +112,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator
|
|||||||
client,
|
client,
|
||||||
model: req.model,
|
model: req.model,
|
||||||
messages: req.messages,
|
messages: req.messages,
|
||||||
|
enabledTools,
|
||||||
userLocation: req.userLocation,
|
userLocation: req.userLocation,
|
||||||
temperature: req.temperature,
|
temperature: req.temperature,
|
||||||
maxTokens: req.maxTokens,
|
maxTokens: req.maxTokens,
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export type MultiplexRequest = {
|
|||||||
provider: Provider;
|
provider: Provider;
|
||||||
model: string;
|
model: string;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
|
additionalSystemPrompt?: string;
|
||||||
|
enabledTools?: string[];
|
||||||
userLocation?: string;
|
userLocation?: string;
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { env } from "./env.js";
|
|||||||
import { buildComparableAttachments } from "./llm/message-content.js";
|
import { buildComparableAttachments } from "./llm/message-content.js";
|
||||||
import { runMultiplex } from "./llm/multiplexer.js";
|
import { runMultiplex } from "./llm/multiplexer.js";
|
||||||
import { runMultiplexStream, type StreamEvent } from "./llm/streaming.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 { getModelCatalogSnapshot } from "./llm/model-catalog.js";
|
||||||
import { openaiClient } from "./llm/providers.js";
|
import { openaiClient } from "./llm/providers.js";
|
||||||
import { serializeProviderFields, toPrismaProvider } from "./llm/provider-ids.js";
|
import { serializeProviderFields, toPrismaProvider } from "./llm/provider-ids.js";
|
||||||
@@ -15,6 +16,8 @@ import { exaClient } from "./search/exa.js";
|
|||||||
import type { ChatAttachment } from "./llm/types.js";
|
import type { ChatAttachment } from "./llm/types.js";
|
||||||
|
|
||||||
const ProviderSchema = z.enum(["openai", "anthropic", "xai", "hermes-agent"]);
|
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 = {
|
type IncomingChatMessage = {
|
||||||
role: "system" | "user" | "assistant" | "tool";
|
role: "system" | "user" | "assistant" | "tool";
|
||||||
@@ -168,6 +171,8 @@ const CompletionStreamBody = z
|
|||||||
provider: ProviderSchema,
|
provider: ProviderSchema,
|
||||||
model: z.string().min(1),
|
model: z.string().min(1),
|
||||||
messages: z.array(CompletionMessageSchema),
|
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(),
|
userLocation: z.string().trim().min(1).max(200).optional(),
|
||||||
temperature: z.number().min(0).max(2).optional(),
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
maxTokens: z.number().int().positive().optional(),
|
maxTokens: z.number().int().positive().optional(),
|
||||||
@@ -193,6 +198,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({
|
const SearchRunBody = z.object({
|
||||||
query: z.string().trim().min(1).optional(),
|
query: z.string().trim().min(1).optional(),
|
||||||
title: z.string().trim().min(1).optional(),
|
title: z.string().trim().min(1).optional(),
|
||||||
@@ -382,6 +422,8 @@ async function listWorkspaceItems() {
|
|||||||
initiatedModel: true,
|
initiatedModel: true,
|
||||||
lastUsedProvider: true,
|
lastUsedProvider: true,
|
||||||
lastUsedModel: true,
|
lastUsedModel: true,
|
||||||
|
additionalSystemPrompt: true,
|
||||||
|
enabledTools: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.search.findMany({
|
prisma.search.findMany({
|
||||||
@@ -641,6 +683,11 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
return { providers: getModelCatalogSnapshot() };
|
return { providers: getModelCatalogSnapshot() };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/v1/chat-tools", async (req) => {
|
||||||
|
requireAdmin(req);
|
||||||
|
return { tools: getAvailableChatTools() };
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/v1/active-runs", async (req) => {
|
app.get("/v1/active-runs", async (req) => {
|
||||||
requireAdmin(req);
|
requireAdmin(req);
|
||||||
return {
|
return {
|
||||||
@@ -668,6 +715,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
initiatedModel: true,
|
initiatedModel: true,
|
||||||
lastUsedProvider: true,
|
lastUsedProvider: true,
|
||||||
lastUsedModel: true,
|
lastUsedModel: true,
|
||||||
|
additionalSystemPrompt: true,
|
||||||
|
enabledTools: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { chats: chats.map((chat) => serializeProviderFields(chat)) };
|
return { chats: chats.map((chat) => serializeProviderFields(chat)) };
|
||||||
@@ -680,6 +729,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
provider: ProviderSchema.optional(),
|
provider: ProviderSchema.optional(),
|
||||||
model: z.string().trim().min(1).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(),
|
messages: z.array(CompletionMessageSchema).optional(),
|
||||||
})
|
})
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
@@ -708,6 +759,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
initiatedModel: body.model,
|
initiatedModel: body.model,
|
||||||
lastUsedProvider: body.provider ? (toPrismaProvider(body.provider) as any) : undefined,
|
lastUsedProvider: body.provider ? (toPrismaProvider(body.provider) as any) : undefined,
|
||||||
lastUsedModel: body.model,
|
lastUsedModel: body.model,
|
||||||
|
additionalSystemPrompt: normalizeAdditionalSystemPrompt(body.additionalSystemPrompt),
|
||||||
|
enabledTools: body.enabledTools as any,
|
||||||
messages: body.messages?.length
|
messages: body.messages?.length
|
||||||
? {
|
? {
|
||||||
create: body.messages.map((message) => ({
|
create: body.messages.map((message) => ({
|
||||||
@@ -728,6 +781,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
initiatedModel: true,
|
initiatedModel: true,
|
||||||
lastUsedProvider: true,
|
lastUsedProvider: true,
|
||||||
lastUsedModel: true,
|
lastUsedModel: true,
|
||||||
|
additionalSystemPrompt: true,
|
||||||
|
enabledTools: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { chat: serializeProviderFields(chat) };
|
return { chat: serializeProviderFields(chat) };
|
||||||
@@ -736,13 +791,22 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
app.patch("/v1/chats/:chatId", async (req) => {
|
app.patch("/v1/chats/:chatId", async (req) => {
|
||||||
requireAdmin(req);
|
requireAdmin(req);
|
||||||
const Params = z.object({ chatId: z.string() });
|
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 { chatId } = Params.parse(req.params);
|
||||||
const body = Body.parse(req.body ?? {});
|
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({
|
const updated = await prisma.chat.updateMany({
|
||||||
where: { id: chatId },
|
where: { id: chatId },
|
||||||
data: { title: body.title },
|
data: data as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updated.count === 0) return app.httpErrors.notFound("chat not found");
|
if (updated.count === 0) return app.httpErrors.notFound("chat not found");
|
||||||
@@ -758,6 +822,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
initiatedModel: true,
|
initiatedModel: true,
|
||||||
lastUsedProvider: true,
|
lastUsedProvider: true,
|
||||||
lastUsedModel: true,
|
lastUsedModel: true,
|
||||||
|
additionalSystemPrompt: true,
|
||||||
|
enabledTools: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!chat) return app.httpErrors.notFound("chat not found");
|
if (!chat) return app.httpErrors.notFound("chat not found");
|
||||||
@@ -783,6 +849,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
initiatedModel: true,
|
initiatedModel: true,
|
||||||
lastUsedProvider: true,
|
lastUsedProvider: true,
|
||||||
lastUsedModel: true,
|
lastUsedModel: true,
|
||||||
|
additionalSystemPrompt: true,
|
||||||
|
enabledTools: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!existing) return app.httpErrors.notFound("chat not found");
|
if (!existing) return app.httpErrors.notFound("chat not found");
|
||||||
@@ -804,6 +872,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
initiatedModel: true,
|
initiatedModel: true,
|
||||||
lastUsedProvider: true,
|
lastUsedProvider: true,
|
||||||
lastUsedModel: true,
|
lastUsedModel: true,
|
||||||
|
additionalSystemPrompt: true,
|
||||||
|
enabledTools: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -924,6 +994,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
initiatedModel: true,
|
initiatedModel: true,
|
||||||
lastUsedProvider: true,
|
lastUsedProvider: true,
|
||||||
lastUsedModel: true,
|
lastUsedModel: true,
|
||||||
|
additionalSystemPrompt: true,
|
||||||
|
enabledTools: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1123,6 +1195,8 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
provider: ProviderSchema,
|
provider: ProviderSchema,
|
||||||
model: z.string().min(1),
|
model: z.string().min(1),
|
||||||
messages: z.array(CompletionMessageSchema),
|
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(),
|
userLocation: z.string().trim().min(1).max(200).optional(),
|
||||||
temperature: z.number().min(0).max(2).optional(),
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
maxTokens: z.number().int().positive().optional(),
|
maxTokens: z.number().int().positive().optional(),
|
||||||
@@ -1143,7 +1217,7 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
await storeNonAssistantMessages(body.chatId, body.messages);
|
await storeNonAssistantMessages(body.chatId, body.messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runMultiplex(body);
|
const result = await runMultiplex(await applyStoredChatSettings(body));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatId: body.chatId ?? null,
|
chatId: body.chatId ?? null,
|
||||||
@@ -1174,14 +1248,14 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
if (activeChatStreams.has(body.chatId)) {
|
if (activeChatStreams.has(body.chatId)) {
|
||||||
return app.httpErrors.conflict("chat completion already running");
|
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);
|
return streamActiveRun(req, reply, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.raw.writeHead(200, buildSseHeaders(typeof req.headers.origin === "string" ? req.headers.origin : undefined));
|
reply.raw.writeHead(200, buildSseHeaders(typeof req.headers.origin === "string" ? req.headers.origin : undefined));
|
||||||
reply.raw.flushHeaders();
|
reply.raw.flushHeaders();
|
||||||
|
|
||||||
for await (const ev of runMultiplexStream(body)) {
|
for await (const ev of runMultiplexStream(await applyStoredChatSettings(body))) {
|
||||||
writeSseEvent(reply, mapChatStreamEvent(ev));
|
writeSseEvent(reply, mapChatStreamEvent(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import { Check, ChevronDown, Globe2, LoaderCircle, Menu, MessageSquare, Paperclip, Plus, Rabbit, Search, SendHorizontal, Trash2, X } from "lucide-preact";
|
import {
|
||||||
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
Globe2,
|
||||||
|
LoaderCircle,
|
||||||
|
Menu,
|
||||||
|
MessageSquare,
|
||||||
|
Paperclip,
|
||||||
|
Plus,
|
||||||
|
Rabbit,
|
||||||
|
Search,
|
||||||
|
SendHorizontal,
|
||||||
|
Settings2,
|
||||||
|
Trash2,
|
||||||
|
X,
|
||||||
|
} from "lucide-preact";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@@ -18,12 +33,14 @@ import {
|
|||||||
attachSearchStream,
|
attachSearchStream,
|
||||||
getActiveRuns,
|
getActiveRuns,
|
||||||
getChat,
|
getChat,
|
||||||
|
listChatTools,
|
||||||
listModels,
|
listModels,
|
||||||
getSearch,
|
getSearch,
|
||||||
listWorkspaceItems,
|
listWorkspaceItems,
|
||||||
runCompletionStream,
|
runCompletionStream,
|
||||||
runSearchStream,
|
runSearchStream,
|
||||||
suggestChatTitle,
|
suggestChatTitle,
|
||||||
|
updateChatSettings,
|
||||||
getMessageAttachments,
|
getMessageAttachments,
|
||||||
type ChatAttachment,
|
type ChatAttachment,
|
||||||
type ActiveRunsResponse,
|
type ActiveRunsResponse,
|
||||||
@@ -31,6 +48,7 @@ import {
|
|||||||
type Provider,
|
type Provider,
|
||||||
type ChatDetail,
|
type ChatDetail,
|
||||||
type ChatSummary,
|
type ChatSummary,
|
||||||
|
type ChatToolInfo,
|
||||||
type CompletionRequestMessage,
|
type CompletionRequestMessage,
|
||||||
type Message,
|
type Message,
|
||||||
type SearchDetail,
|
type SearchDetail,
|
||||||
@@ -371,6 +389,30 @@ function getProviderLabel(provider: Provider | null | undefined) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getToolLabel(name: string) {
|
||||||
|
if (name === "web_search") return "Web search";
|
||||||
|
if (name === "fetch_url") return "Fetch URL";
|
||||||
|
if (name === "codex_exec") return "Codex";
|
||||||
|
if (name === "shell_exec") return "Shell";
|
||||||
|
return name
|
||||||
|
.split("_")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultEnabledTools(availableTools: ChatToolInfo[]) {
|
||||||
|
return availableTools.map((tool) => tool.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEnabledTools(value: unknown, availableTools: ChatToolInfo[]) {
|
||||||
|
const available = new Set(availableTools.map((tool) => tool.name));
|
||||||
|
if (!Array.isArray(value)) return getDefaultEnabledTools(availableTools);
|
||||||
|
return [...new Set(value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean))].filter((name) =>
|
||||||
|
available.has(name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getChatModelSelection(chat: Pick<ChatSummary, "lastUsedProvider" | "lastUsedModel"> | Pick<ChatDetail, "lastUsedProvider" | "lastUsedModel"> | null) {
|
function getChatModelSelection(chat: Pick<ChatSummary, "lastUsedProvider" | "lastUsedModel"> | Pick<ChatDetail, "lastUsedProvider" | "lastUsedModel"> | null) {
|
||||||
if (!chat?.lastUsedProvider || !chat.lastUsedModel?.trim()) return null;
|
if (!chat?.lastUsedProvider || !chat.lastUsedModel?.trim()) return null;
|
||||||
return {
|
return {
|
||||||
@@ -730,6 +772,7 @@ export default function App() {
|
|||||||
const [isComposerDropActive, setIsComposerDropActive] = useState(false);
|
const [isComposerDropActive, setIsComposerDropActive] = useState(false);
|
||||||
const [provider, setProvider] = useState<Provider>("openai");
|
const [provider, setProvider] = useState<Provider>("openai");
|
||||||
const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse["providers"]>(EMPTY_MODEL_CATALOG);
|
const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse["providers"]>(EMPTY_MODEL_CATALOG);
|
||||||
|
const [availableChatTools, setAvailableChatTools] = useState<ChatToolInfo[]>([]);
|
||||||
const [providerModelPreferences, setProviderModelPreferences] = useState<ProviderModelPreferences>(() => loadStoredModelPreferences());
|
const [providerModelPreferences, setProviderModelPreferences] = useState<ProviderModelPreferences>(() => loadStoredModelPreferences());
|
||||||
const [model, setModel] = useState(() => {
|
const [model, setModel] = useState(() => {
|
||||||
const stored = loadStoredModelPreferences();
|
const stored = loadStoredModelPreferences();
|
||||||
@@ -752,6 +795,9 @@ export default function App() {
|
|||||||
const [isConvertingQuickQuestion, setIsConvertingQuickQuestion] = useState(false);
|
const [isConvertingQuickQuestion, setIsConvertingQuickQuestion] = useState(false);
|
||||||
const [quickQuestionError, setQuickQuestionError] = useState<string | null>(null);
|
const [quickQuestionError, setQuickQuestionError] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isChatSettingsOpen, setIsChatSettingsOpen] = useState(false);
|
||||||
|
const [additionalSystemPrompt, setAdditionalSystemPrompt] = useState("");
|
||||||
|
const [enabledTools, setEnabledTools] = useState<string[]>([]);
|
||||||
const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
|
const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
|
||||||
const transcriptContainerRef = useRef<HTMLDivElement>(null);
|
const transcriptContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const transcriptEndRef = useRef<HTMLDivElement>(null);
|
const transcriptEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -876,6 +922,9 @@ export default function App() {
|
|||||||
searchRunCountersRef.current.clear();
|
searchRunCountersRef.current.clear();
|
||||||
setComposer("");
|
setComposer("");
|
||||||
setPendingAttachments([]);
|
setPendingAttachments([]);
|
||||||
|
setIsChatSettingsOpen(false);
|
||||||
|
setAdditionalSystemPrompt("");
|
||||||
|
setEnabledTools([]);
|
||||||
setIsQuickQuestionOpen(false);
|
setIsQuickQuestionOpen(false);
|
||||||
setQuickPrompt("");
|
setQuickPrompt("");
|
||||||
setQuickSubmittedPrompt(null);
|
setQuickSubmittedPrompt(null);
|
||||||
@@ -940,6 +989,21 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshChatTools = async () => {
|
||||||
|
try {
|
||||||
|
const tools = await listChatTools();
|
||||||
|
setAvailableChatTools(tools);
|
||||||
|
setEnabledTools((current) => normalizeEnabledTools(current.length ? current : null, tools));
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
if (message.includes("bearer token")) {
|
||||||
|
handleAuthFailure(message);
|
||||||
|
} else {
|
||||||
|
setError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshActiveRuns = async () => {
|
const refreshActiveRuns = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getActiveRuns();
|
const data = await getActiveRuns();
|
||||||
@@ -992,7 +1056,7 @@ export default function App() {
|
|||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
const preferredSelection = initialRouteSelectionRef.current;
|
const preferredSelection = initialRouteSelectionRef.current;
|
||||||
initialRouteSelectionRef.current = null;
|
initialRouteSelectionRef.current = null;
|
||||||
void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshActiveRuns()]);
|
void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshChatTools(), refreshActiveRuns()]);
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1254,6 +1318,19 @@ export default function App() {
|
|||||||
setModel(nextSelection.model);
|
setModel(nextSelection.model);
|
||||||
}, [draftKind, selectedChat, selectedChatSummary, selectedItem]);
|
}, [draftKind, selectedChat, selectedChatSummary, selectedItem]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (draftKind === "chat") {
|
||||||
|
setAdditionalSystemPrompt("");
|
||||||
|
setEnabledTools(getDefaultEnabledTools(availableChatTools));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedItem?.kind !== "chat") return;
|
||||||
|
const chat = selectedChat?.id === selectedItem.id ? selectedChat : selectedChatSummary;
|
||||||
|
if (!chat) return;
|
||||||
|
setAdditionalSystemPrompt(chat.additionalSystemPrompt ?? "");
|
||||||
|
setEnabledTools(normalizeEnabledTools(chat.enabledTools, availableChatTools));
|
||||||
|
}, [availableChatTools, draftKind, selectedChat, selectedChatSummary, selectedItem]);
|
||||||
|
|
||||||
const selectedTitle = useMemo(() => {
|
const selectedTitle = useMemo(() => {
|
||||||
if (draftKind === "chat") return "New chat";
|
if (draftKind === "chat") return "New chat";
|
||||||
if (draftKind === "search") return "New search";
|
if (draftKind === "search") return "New search";
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export type ChatSummary = {
|
|||||||
initiatedModel: string | null;
|
initiatedModel: string | null;
|
||||||
lastUsedProvider: Provider | null;
|
lastUsedProvider: Provider | null;
|
||||||
lastUsedModel: string | null;
|
lastUsedModel: string | null;
|
||||||
|
additionalSystemPrompt: string | null;
|
||||||
|
enabledTools: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchSummary = {
|
export type SearchSummary = {
|
||||||
@@ -58,6 +60,8 @@ export type ChatDetail = {
|
|||||||
initiatedModel: string | null;
|
initiatedModel: string | null;
|
||||||
lastUsedProvider: Provider | null;
|
lastUsedProvider: Provider | null;
|
||||||
lastUsedModel: string | null;
|
lastUsedModel: string | null;
|
||||||
|
additionalSystemPrompt: string | null;
|
||||||
|
enabledTools: string[] | null;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,6 +153,11 @@ export type ModelCatalogResponse = {
|
|||||||
providers: Partial<Record<Provider, ProviderModelInfo>>;
|
providers: Partial<Record<Provider, ProviderModelInfo>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChatToolInfo = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ActiveRunsResponse = {
|
export type ActiveRunsResponse = {
|
||||||
chats: string[];
|
chats: string[];
|
||||||
searches: string[];
|
searches: string[];
|
||||||
@@ -174,6 +183,8 @@ type CreateChatRequest = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
provider?: Provider;
|
provider?: Provider;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
additionalSystemPrompt?: string;
|
||||||
|
enabledTools?: string[];
|
||||||
messages?: CompletionRequestMessage[];
|
messages?: CompletionRequestMessage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,6 +248,11 @@ export async function listModels() {
|
|||||||
return api<ModelCatalogResponse>("/v1/models");
|
return api<ModelCatalogResponse>("/v1/models");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listChatTools() {
|
||||||
|
const data = await api<{ tools: ChatToolInfo[] }>("/v1/chat-tools");
|
||||||
|
return data.tools;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getActiveRuns() {
|
export async function getActiveRuns() {
|
||||||
return api<ActiveRunsResponse>("/v1/active-runs");
|
return api<ActiveRunsResponse>("/v1/active-runs");
|
||||||
}
|
}
|
||||||
@@ -263,6 +279,14 @@ export async function updateChatTitle(chatId: string, title: string) {
|
|||||||
return data.chat;
|
return data.chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateChatSettings(chatId: string, body: { additionalSystemPrompt?: string | null; enabledTools?: string[] }) {
|
||||||
|
const data = await api<{ chat: ChatSummary }>(`/v1/chats/${chatId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
return data.chat;
|
||||||
|
}
|
||||||
|
|
||||||
export async function suggestChatTitle(body: { chatId: string; content: string }) {
|
export async function suggestChatTitle(body: { chatId: string; content: string }) {
|
||||||
const data = await api<{ chat: ChatSummary }>("/v1/chats/title/suggest", {
|
const data = await api<{ chat: ChatSummary }>("/v1/chats/title/suggest", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -569,6 +593,8 @@ export async function runCompletion(body: {
|
|||||||
provider: Provider;
|
provider: Provider;
|
||||||
model: string;
|
model: string;
|
||||||
messages: CompletionRequestMessage[];
|
messages: CompletionRequestMessage[];
|
||||||
|
additionalSystemPrompt?: string;
|
||||||
|
enabledTools?: string[];
|
||||||
userLocation?: string;
|
userLocation?: string;
|
||||||
}) {
|
}) {
|
||||||
return api<CompletionResponse>("/v1/chat-completions", {
|
return api<CompletionResponse>("/v1/chat-completions", {
|
||||||
@@ -584,6 +610,8 @@ export async function runCompletionStream(
|
|||||||
provider: Provider;
|
provider: Provider;
|
||||||
model: string;
|
model: string;
|
||||||
messages: CompletionRequestMessage[];
|
messages: CompletionRequestMessage[];
|
||||||
|
additionalSystemPrompt?: string;
|
||||||
|
enabledTools?: string[];
|
||||||
userLocation?: string;
|
userLocation?: string;
|
||||||
},
|
},
|
||||||
handlers: CompletionStreamHandlers,
|
handlers: CompletionStreamHandlers,
|
||||||
|
|||||||
Reference in New Issue
Block a user