mirror of
https://github.com/LostRuins/koboldcpp.git
synced 2026-05-17 04:09:19 +00:00
* webui: Move static build output from `tools/server/public` to `build/ui` directory * refactor: Move to `tools/ui` * refactor: rename CMake variables and preprocessor defines - Rename LLAMA_BUILD_WEBUI -> LLAMA_BUILD_UI (old kept as deprecated) - Rename LLAMA_USE_PREBUILT_WEBUI -> LLAMA_USE_PREBUILT_UI (old kept as deprecated) - Backward compat: old vars auto-forward to new ones with DEPRECATION warning - Rename internal vars: WEBUI_SOURCE -> UI_SOURCE, WEBUI_SOURCE_DIR -> UI_SOURCE_DIR, etc. - Rename HF bucket: LLAMA_WEBUI_HF_BUCKET -> LLAMA_UI_HF_BUCKET - Emit both LLAMA_BUILD_WEBUI and LLAMA_BUILD_UI preprocessor defines - Emit both LLAMA_WEBUI_DEFAULT_ENABLED and LLAMA_UI_DEFAULT_ENABLED * refactor: rename CLI flags (--webui -> --ui) with backward compat - Add --ui/--no-ui (old --webui/--no-webui kept as deprecated aliases) - Add --ui-config (old --webui-config kept as deprecated alias) - Add --ui-config-file (old --webui-config-file kept as deprecated alias) - Add --ui-mcp-proxy/--no-ui-mcp-proxy (old --webui-mcp-proxy kept as deprecated) - Add new env vars: LLAMA_ARG_UI, LLAMA_ARG_UI_CONFIG, LLAMA_ARG_UI_CONFIG_FILE, LLAMA_ARG_UI_MCP_PROXY - C++ struct fields: params.ui, params.ui_config_json, params.ui_mcp_proxy added alongside old fields - Backward compat: old fields synced to new ones in g_params_to_internals * refactor: update C++ server internals with backward compat - Rename json_webui_settings -> json_ui_settings (both kept in server_context_meta) - Rename params.webui usage -> params.ui (both synced, old still works) - JSON API emits both "ui"/"ui_settings" and "webui"/"webui_settings" keys - Server routes use params.ui_mcp_proxy || params.webui_mcp_proxy - Preprocessor guards use #if defined(LLAMA_BUILD_UI) || defined(LLAMA_BUILD_WEBUI) * refactor: rename CI/CD workflows, artifacts, and build script - Rename webui-build.yml -> ui-build.yml; artifact webui-build -> ui-build - Rename webui-publish.yml -> ui-publish.yml; var HF_BUCKET_WEBUI_STATIC_OUTPUT -> HF_BUCKET_UI_STATIC_OUTPUT - Rename server-webui.yml -> server-ui.yml; job webui-build/checks -> ui-build/checks - Update server.yml: job/artifact refs webui-build -> ui-build - Update release.yml: all webui-build/publish refs -> ui-build/publish; HF_TOKEN_WEBUI_STATIC_OUTPUT -> HF_TOKEN_UI_STATIC_OUTPUT - Update server-self-hosted.yml: webui-build -> ui-build - Update build-self-hosted.yml: HF_WEBUI_VERSION -> HF_UI_VERSION - Rename webui-download.cmake -> ui-download.cmake (internal refs updated) - Update labeler.yml: server/webui -> server/ui path label * docs: update CODEOWNERS and server README docs - Update CODEOWNERS: team ggml-org/llama-webui -> ggml-org/llama-ui, path /tools/server/webui/ -> /tools/ui/ - Update server README.md: CLI tables show --ui flags with deprecated --webui aliases - Update server README-dev.md: "WebUI" -> "UI", paths updated to tools/ui/ * fix: Small fixes for UI build * fix: CMake.txt syntax * chore: Formatting * fix: `.editorconfig` for llama-ui * chore: Formatting * refactor: Use `APP_NAME` in Error route * refactor: Cleanup * refactor: Single migration service * make llama-ui a linkable target * fix: UI Build output * fix: Missing change * fix: separate llama-ui npm build output into build/tools/ui/dist subfolder + use cmake npm build instead of downloading ui-build.yml artifacts in CI * refactor: UI workflows cleanup --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
257 lines
8.5 KiB
TypeScript
257 lines
8.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { deriveAgenticSections, hasAgenticContent } from '$lib/utils/agentic';
|
|
import { AgenticSectionType, MessageRole } from '$lib/enums';
|
|
import type { DatabaseMessage } from '$lib/types/database';
|
|
import type { ApiChatCompletionToolCall } from '$lib/types/api';
|
|
|
|
function makeAssistant(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {
|
|
return {
|
|
id: overrides.id ?? 'ast-1',
|
|
convId: 'conv-1',
|
|
type: 'text',
|
|
timestamp: Date.now(),
|
|
role: MessageRole.ASSISTANT,
|
|
content: overrides.content ?? '',
|
|
parent: null,
|
|
children: [],
|
|
...overrides
|
|
} as DatabaseMessage;
|
|
}
|
|
|
|
function makeToolMsg(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {
|
|
return {
|
|
id: overrides.id ?? 'tool-1',
|
|
convId: 'conv-1',
|
|
type: 'text',
|
|
timestamp: Date.now(),
|
|
role: MessageRole.TOOL,
|
|
content: overrides.content ?? 'tool result',
|
|
parent: null,
|
|
children: [],
|
|
toolCallId: overrides.toolCallId ?? 'call_1',
|
|
...overrides
|
|
} as DatabaseMessage;
|
|
}
|
|
|
|
describe('deriveAgenticSections', () => {
|
|
it('returns empty array for assistant with no content', () => {
|
|
const msg = makeAssistant({ content: '' });
|
|
const sections = deriveAgenticSections(msg);
|
|
expect(sections).toEqual([]);
|
|
});
|
|
|
|
it('returns text section for simple assistant message', () => {
|
|
const msg = makeAssistant({ content: 'Hello world' });
|
|
const sections = deriveAgenticSections(msg);
|
|
expect(sections).toHaveLength(1);
|
|
expect(sections[0].type).toBe(AgenticSectionType.TEXT);
|
|
expect(sections[0].content).toBe('Hello world');
|
|
});
|
|
|
|
it('returns reasoning + text for message with reasoning', () => {
|
|
const msg = makeAssistant({
|
|
content: 'Answer is 4.',
|
|
reasoningContent: 'Let me think...'
|
|
});
|
|
const sections = deriveAgenticSections(msg);
|
|
expect(sections).toHaveLength(2);
|
|
expect(sections[0].type).toBe(AgenticSectionType.REASONING);
|
|
expect(sections[0].content).toBe('Let me think...');
|
|
expect(sections[1].type).toBe(AgenticSectionType.TEXT);
|
|
});
|
|
|
|
it('single turn: assistant with tool calls and results', () => {
|
|
const msg = makeAssistant({
|
|
content: 'Let me check.',
|
|
toolCalls: JSON.stringify([
|
|
{
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: { name: 'search', arguments: '{"q":"test"}' }
|
|
}
|
|
])
|
|
});
|
|
const toolResult = makeToolMsg({
|
|
toolCallId: 'call_1',
|
|
content: 'Found 3 results'
|
|
});
|
|
const sections = deriveAgenticSections(msg, [toolResult]);
|
|
expect(sections).toHaveLength(2);
|
|
expect(sections[0].type).toBe(AgenticSectionType.TEXT);
|
|
expect(sections[1].type).toBe(AgenticSectionType.TOOL_CALL);
|
|
expect(sections[1].toolName).toBe('search');
|
|
expect(sections[1].toolResult).toBe('Found 3 results');
|
|
});
|
|
|
|
it('single turn: pending tool call without result', () => {
|
|
const msg = makeAssistant({
|
|
toolCalls: JSON.stringify([
|
|
{ id: 'call_1', type: 'function', function: { name: 'bash', arguments: '{}' } }
|
|
])
|
|
});
|
|
const sections = deriveAgenticSections(msg, [], [], true);
|
|
expect(sections).toHaveLength(1);
|
|
expect(sections[0].type).toBe(AgenticSectionType.TOOL_CALL_PENDING);
|
|
expect(sections[0].toolName).toBe('bash');
|
|
});
|
|
|
|
it('multi-turn: two assistant turns grouped as one session', () => {
|
|
const assistant1 = makeAssistant({
|
|
id: 'ast-1',
|
|
content: 'Turn 1 text',
|
|
toolCalls: JSON.stringify([
|
|
{
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: { name: 'search', arguments: '{"q":"foo"}' }
|
|
}
|
|
])
|
|
});
|
|
const tool1 = makeToolMsg({ id: 'tool-1', toolCallId: 'call_1', content: 'result 1' });
|
|
const assistant2 = makeAssistant({
|
|
id: 'ast-2',
|
|
content: 'Final answer based on results.'
|
|
});
|
|
|
|
// toolMessages contains both tool result and continuation assistant
|
|
const sections = deriveAgenticSections(assistant1, [tool1, assistant2]);
|
|
expect(sections).toHaveLength(3);
|
|
// Turn 1
|
|
expect(sections[0].type).toBe(AgenticSectionType.TEXT);
|
|
expect(sections[0].content).toBe('Turn 1 text');
|
|
expect(sections[1].type).toBe(AgenticSectionType.TOOL_CALL);
|
|
expect(sections[1].toolName).toBe('search');
|
|
expect(sections[1].toolResult).toBe('result 1');
|
|
// Turn 2 (final)
|
|
expect(sections[2].type).toBe(AgenticSectionType.TEXT);
|
|
expect(sections[2].content).toBe('Final answer based on results.');
|
|
});
|
|
|
|
it('multi-turn: three turns with tool calls', () => {
|
|
const assistant1 = makeAssistant({
|
|
id: 'ast-1',
|
|
content: '',
|
|
toolCalls: JSON.stringify([
|
|
{
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: { name: 'list_files', arguments: '{}' }
|
|
}
|
|
])
|
|
});
|
|
const tool1 = makeToolMsg({ id: 'tool-1', toolCallId: 'call_1', content: 'file1 file2' });
|
|
const assistant2 = makeAssistant({
|
|
id: 'ast-2',
|
|
content: 'Reading file1...',
|
|
toolCalls: JSON.stringify([
|
|
{
|
|
id: 'call_2',
|
|
type: 'function',
|
|
function: { name: 'read_file', arguments: '{"path":"file1"}' }
|
|
}
|
|
])
|
|
});
|
|
const tool2 = makeToolMsg({
|
|
id: 'tool-2',
|
|
toolCallId: 'call_2',
|
|
content: 'contents of file1'
|
|
});
|
|
const assistant3 = makeAssistant({
|
|
id: 'ast-3',
|
|
content: 'Here is the analysis.',
|
|
reasoningContent: 'The file contains...'
|
|
});
|
|
|
|
const sections = deriveAgenticSections(assistant1, [tool1, assistant2, tool2, assistant3]);
|
|
// Turn 1: tool_call (no text since content is empty)
|
|
// Turn 2: text + tool_call
|
|
// Turn 3: reasoning + text
|
|
expect(sections).toHaveLength(5);
|
|
expect(sections[0].type).toBe(AgenticSectionType.TOOL_CALL);
|
|
expect(sections[0].toolName).toBe('list_files');
|
|
expect(sections[1].type).toBe(AgenticSectionType.TEXT);
|
|
expect(sections[1].content).toBe('Reading file1...');
|
|
expect(sections[2].type).toBe(AgenticSectionType.TOOL_CALL);
|
|
expect(sections[2].toolName).toBe('read_file');
|
|
expect(sections[3].type).toBe(AgenticSectionType.REASONING);
|
|
expect(sections[4].type).toBe(AgenticSectionType.TEXT);
|
|
expect(sections[4].content).toBe('Here is the analysis.');
|
|
});
|
|
|
|
it('returns REASONING_PENDING when streaming with only reasoning content', () => {
|
|
const msg = makeAssistant({
|
|
reasoningContent: 'Let me think about this...'
|
|
});
|
|
const sections = deriveAgenticSections(msg, [], [], true);
|
|
expect(sections).toHaveLength(1);
|
|
expect(sections[0].type).toBe(AgenticSectionType.REASONING_PENDING);
|
|
expect(sections[0].content).toBe('Let me think about this...');
|
|
});
|
|
|
|
it('returns REASONING (not pending) when streaming but text content has appeared', () => {
|
|
const msg = makeAssistant({
|
|
content: 'The answer is',
|
|
reasoningContent: 'Let me think...'
|
|
});
|
|
const sections = deriveAgenticSections(msg, [], [], true);
|
|
expect(sections).toHaveLength(2);
|
|
expect(sections[0].type).toBe(AgenticSectionType.REASONING);
|
|
expect(sections[1].type).toBe(AgenticSectionType.TEXT);
|
|
});
|
|
|
|
it('returns REASONING (not pending) when not streaming', () => {
|
|
const msg = makeAssistant({
|
|
reasoningContent: 'Let me think...'
|
|
});
|
|
const sections = deriveAgenticSections(msg, [], [], false);
|
|
expect(sections).toHaveLength(1);
|
|
expect(sections[0].type).toBe(AgenticSectionType.REASONING);
|
|
});
|
|
|
|
it('multi-turn: streaming tool calls on last turn', () => {
|
|
const assistant1 = makeAssistant({
|
|
toolCalls: JSON.stringify([
|
|
{ id: 'call_1', type: 'function', function: { name: 'search', arguments: '{}' } }
|
|
])
|
|
});
|
|
const tool1 = makeToolMsg({ toolCallId: 'call_1', content: 'result' });
|
|
const assistant2 = makeAssistant({ id: 'ast-2', content: '' });
|
|
|
|
const streamingToolCalls: ApiChatCompletionToolCall[] = [
|
|
{ id: 'call_2', type: 'function', function: { name: 'write_file', arguments: '{"pa' } }
|
|
];
|
|
|
|
const sections = deriveAgenticSections(assistant1, [tool1, assistant2], streamingToolCalls);
|
|
// Turn 1: tool_call
|
|
// Turn 2 (streaming): streaming tool call
|
|
expect(sections.some((s) => s.type === AgenticSectionType.TOOL_CALL)).toBe(true);
|
|
expect(sections.some((s) => s.type === AgenticSectionType.TOOL_CALL_STREAMING)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('hasAgenticContent', () => {
|
|
it('returns false for plain assistant', () => {
|
|
const msg = makeAssistant({ content: 'Just text' });
|
|
expect(hasAgenticContent(msg)).toBe(false);
|
|
});
|
|
|
|
it('returns true when message has toolCalls', () => {
|
|
const msg = makeAssistant({
|
|
toolCalls: JSON.stringify([
|
|
{ id: 'call_1', type: 'function', function: { name: 'test', arguments: '{}' } }
|
|
])
|
|
});
|
|
expect(hasAgenticContent(msg)).toBe(true);
|
|
});
|
|
|
|
it('returns true when toolMessages are provided', () => {
|
|
const msg = makeAssistant();
|
|
const tool = makeToolMsg();
|
|
expect(hasAgenticContent(msg, [tool])).toBe(true);
|
|
});
|
|
|
|
it('returns false for empty toolCalls JSON', () => {
|
|
const msg = makeAssistant({ toolCalls: '[]' });
|
|
expect(hasAgenticContent(msg)).toBe(false);
|
|
});
|
|
});
|