kiku-agent 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -12
- package/package.json +5 -12
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +288 -0
- package/server/agent/eval/modules/evaluation/scoring.js +363 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4799 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +253 -0
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +296 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +627 -0
- package/server/agent/tests/cliAutomation.test.js +158 -0
- package/server/agent/tests/cloudProviders.test.js +322 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +506 -0
- package/server/agent/tests/contextIntelligence.test.js +209 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +284 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +320 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +194 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +324 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +141 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +101 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +44 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +221 -0
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +267 -0
- package/server/agent/tests/sidebarControls.test.js +135 -0
- package/server/agent/tests/statefulMemory.test.js +277 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +197 -0
- package/server/agent/tests/textToolParsing.test.js +187 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +159 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +595 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +551 -0
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +284 -0
- package/server/agent/tests/webSources.test.js +281 -0
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +162 -0
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +63 -0
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ContextCompactor,
|
|
6
|
+
alignChatMessageRoles,
|
|
7
|
+
estimateTextTokens,
|
|
8
|
+
estimateTokenCount,
|
|
9
|
+
prepareChatMessagesForDispatch,
|
|
10
|
+
sanitizePromptString,
|
|
11
|
+
truncatePromptString,
|
|
12
|
+
} from '../contextCompactor.js';
|
|
13
|
+
import { KikuAgent } from '../agentCore.js';
|
|
14
|
+
|
|
15
|
+
function bareAgent() {
|
|
16
|
+
const agent = Object.create(KikuAgent.prototype);
|
|
17
|
+
agent.mcpManager = null;
|
|
18
|
+
agent.allowedToolNames = null;
|
|
19
|
+
agent.conversationHistory = [];
|
|
20
|
+
return agent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('context window split never orphans a tool result from its owning assistant message', () => {
|
|
24
|
+
const compactor = new ContextCompactor({ keepRecentMessages: 3 });
|
|
25
|
+
const history = [
|
|
26
|
+
{ role: 'system', content: 'sys' },
|
|
27
|
+
{ role: 'user', content: 'u1' },
|
|
28
|
+
{ role: 'assistant', content: null, tool_calls: [{ id: 'c1', function: { name: 'web_search', arguments: '{}' } }] },
|
|
29
|
+
{ role: 'tool', tool_call_id: 'c1', name: 'web_search', content: 'result1' },
|
|
30
|
+
{ role: 'user', content: 'u2' },
|
|
31
|
+
{ role: 'assistant', content: 'answer2' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const out = compactor.compact(history, { keepRecentMessages: 3 });
|
|
35
|
+
|
|
36
|
+
assert.equal(out[0].role, 'system');
|
|
37
|
+
const firstKept = out.find(m => m.role !== 'system');
|
|
38
|
+
|
|
39
|
+
assert.equal(firstKept.role, 'assistant');
|
|
40
|
+
assert.ok(Array.isArray(firstKept.tool_calls));
|
|
41
|
+
assert.ok(out.some(m => m.role === 'tool' && m.tool_call_id === 'c1'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('compaction never keeps a dangling tool result at the window boundary', () => {
|
|
45
|
+
const compactor = new ContextCompactor({ keepRecentMessages: 3 });
|
|
46
|
+
const history = [
|
|
47
|
+
{ role: 'system', content: 'sys' },
|
|
48
|
+
{ role: 'tool', name: 'web_search', content: 'dangling' },
|
|
49
|
+
{ role: 'user', content: 'u1' },
|
|
50
|
+
{ role: 'assistant', content: 'answer1' },
|
|
51
|
+
{ role: 'user', content: 'u2' },
|
|
52
|
+
{ role: 'assistant', content: 'answer2' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const out = compactor.compact(history, { keepRecentMessages: 3 });
|
|
56
|
+
const firstKept = out.find(m => m.role !== 'system');
|
|
57
|
+
assert.equal(firstKept.role, 'assistant');
|
|
58
|
+
assert.ok(!out.some(m => m.role === 'tool' && m.content === 'dangling'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('compactAsync prefers the LLM summarizer and falls back to the heuristic on failure', async () => {
|
|
62
|
+
const compactor = new ContextCompactor({ keepRecentMessages: 3 });
|
|
63
|
+
const history = [
|
|
64
|
+
{ role: 'system', content: 'sys' },
|
|
65
|
+
{ role: 'user', content: 'who won the 2026 election of nowhere?' },
|
|
66
|
+
{ role: 'assistant', content: 'no verified data' },
|
|
67
|
+
{ role: 'user', content: 'u2' },
|
|
68
|
+
{ role: 'assistant', content: 'a2' },
|
|
69
|
+
{ role: 'user', content: 'u3' },
|
|
70
|
+
{ role: 'assistant', content: 'a3' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const llmOut = await compactor.compactAsync(history, {
|
|
74
|
+
keepRecentMessages: 3,
|
|
75
|
+
summarizer: async (older) => `LLM summary of ${older.length} messages`,
|
|
76
|
+
});
|
|
77
|
+
assert.equal(llmOut.filter(message => message.role === 'system').length, 1);
|
|
78
|
+
assert.match(llmOut.findLast(message => message.role === 'user').content, /LLM summary of 3 messages/);
|
|
79
|
+
|
|
80
|
+
const fallbackOut = await compactor.compactAsync(history, {
|
|
81
|
+
keepRecentMessages: 3,
|
|
82
|
+
summarizer: async () => { throw new Error('provider down'); },
|
|
83
|
+
});
|
|
84
|
+
assert.match(fallbackOut.findLast(message => message.role === 'user').content, /User asked: "who won the 2026 election of nowhere\?"/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('prompt sanitation removes orphaned surrogates and truncates only at grapheme boundaries', () => {
|
|
88
|
+
assert.equal(sanitizePromptString(`Cafe\u0301\uD800 ok\uDC00`), 'Café ok');
|
|
89
|
+
assert.equal(truncatePromptString('A👩💻B', 2), 'A');
|
|
90
|
+
assert.doesNotMatch(JSON.stringify({ value: truncatePromptString('😀'.repeat(20), 7) }), /\\ud[89ab][0-9a-f]{2}(?!\\ud[c-f][0-9a-f]{2})/i);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('chat role alignment keeps one leading system role and moves directives into the latest user turn', () => {
|
|
94
|
+
const aligned = alignChatMessageRoles([
|
|
95
|
+
{ role: 'system', content: 'immutable persona' },
|
|
96
|
+
{ role: 'user', content: 'first question' },
|
|
97
|
+
{ role: 'assistant', content: 'first answer' },
|
|
98
|
+
{ role: 'system', content: 'reply in Thai' },
|
|
99
|
+
{ role: 'user', content: 'latest question' },
|
|
100
|
+
{ role: 'system', content: 'verify dates' },
|
|
101
|
+
]);
|
|
102
|
+
assert.equal(aligned[0].role, 'system');
|
|
103
|
+
assert.equal(aligned.filter(message => message.role === 'system').length, 1);
|
|
104
|
+
assert.match(aligned.findLast(message => message.role === 'user').content, /reply in Thai[\s\S]*verify dates[\s\S]*latest question/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('dispatch budget subtracts system, generation, and tool definitions before pruning history', () => {
|
|
108
|
+
const history = [
|
|
109
|
+
{ role: 'system', content: 'system '.repeat(80) },
|
|
110
|
+
...Array.from({ length: 8 }, (_, index) => [
|
|
111
|
+
{ role: 'user', content: `question ${index} ` + 'x'.repeat(300) },
|
|
112
|
+
{ role: 'assistant', content: `answer ${index} ` + 'y'.repeat(300) },
|
|
113
|
+
]).flat(),
|
|
114
|
+
];
|
|
115
|
+
const result = prepareChatMessagesForDispatch(history, {
|
|
116
|
+
maxContextTokens: 2048,
|
|
117
|
+
generationReserve: 512,
|
|
118
|
+
tools: [{ type: 'function', function: { name: 'search', description: 'z'.repeat(500) } }],
|
|
119
|
+
});
|
|
120
|
+
assert.equal(result.budget.allocation, 2048 - result.budget.systemTokens - 512 - result.budget.toolDefinitionTokens);
|
|
121
|
+
assert.equal(result.budget.pruned, true);
|
|
122
|
+
assert.ok(estimateTokenCount(result.messages.slice(1)) <= result.budget.threshold);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('_enforceContextBudget trims oldest tool results when KIKU_CONTEXT_LIMIT is exceeded', () => {
|
|
126
|
+
const agent = bareAgent();
|
|
127
|
+
agent.conversationHistory = [
|
|
128
|
+
{ role: 'system', content: 'x'.repeat(50) },
|
|
129
|
+
{ role: 'user', content: 'y'.repeat(50) },
|
|
130
|
+
{ role: 'tool', name: 'web_search', content: JSON.stringify({ payload: 'z'.repeat(2000) }) },
|
|
131
|
+
{ role: 'tool', name: 'get_awards', content: JSON.stringify({ payload: 'q'.repeat(2000) }) },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const previousLimit = process.env.KIKU_CONTEXT_LIMIT;
|
|
135
|
+
process.env.KIKU_CONTEXT_LIMIT = '100';
|
|
136
|
+
try {
|
|
137
|
+
assert.equal(agent._enforceContextBudget(), true);
|
|
138
|
+
assert.match(agent.conversationHistory[2].content, /omitted/);
|
|
139
|
+
assert.match(agent.conversationHistory[3].content, /omitted/);
|
|
140
|
+
assert.equal(agent.conversationHistory[0].content, 'x'.repeat(50));
|
|
141
|
+
assert.equal(agent.conversationHistory[1].content, 'y'.repeat(50));
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
assert.equal(agent._enforceContextBudget(), false);
|
|
145
|
+
} finally {
|
|
146
|
+
if (previousLimit === undefined) delete process.env.KIKU_CONTEXT_LIMIT;
|
|
147
|
+
else process.env.KIKU_CONTEXT_LIMIT = previousLimit;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('_enforceContextBudget is inert without KIKU_CONTEXT_LIMIT', () => {
|
|
152
|
+
const agent = bareAgent();
|
|
153
|
+
const previousLimit = process.env.KIKU_CONTEXT_LIMIT;
|
|
154
|
+
delete process.env.KIKU_CONTEXT_LIMIT;
|
|
155
|
+
try {
|
|
156
|
+
agent.conversationHistory = [{ role: 'user', content: 'hi' }];
|
|
157
|
+
assert.equal(agent._enforceContextBudget(), false);
|
|
158
|
+
assert.equal(agent.conversationHistory[0].content, 'hi');
|
|
159
|
+
} finally {
|
|
160
|
+
if (previousLimit !== undefined) process.env.KIKU_CONTEXT_LIMIT = previousLimit;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('session restore summarizes prior tool evidence instead of dropping it silently', () => {
|
|
165
|
+
const agent = bareAgent();
|
|
166
|
+
agent.session = {
|
|
167
|
+
messages: [
|
|
168
|
+
{ role: 'user', parts: [{ type: 'text', content: 'เธอได้รางวัลอะไรบ้าง?' }] },
|
|
169
|
+
{
|
|
170
|
+
role: 'assistant',
|
|
171
|
+
parts: [
|
|
172
|
+
{ type: 'tool', name: 'get_awards', status: 'ok', result: { count: 3 } },
|
|
173
|
+
{ type: 'text', content: 'เธอได้รางวัล Forbes 30 Under 30 Asia (2019)' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{ role: 'user', parts: [{ type: 'text', content: 'ขอบคุณ' }] },
|
|
177
|
+
{ role: 'assistant', parts: [{ type: 'text', content: 'ด้วยความยินดีจ้ะ' }] },
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const lines = agent._rehydrateSessionMessages();
|
|
182
|
+
assert.equal(lines.length, 1);
|
|
183
|
+
assert.match(lines[0], /get_awards\(ok, 3\)/);
|
|
184
|
+
assert.match(lines[0], /Forbes 30 Under 30/);
|
|
185
|
+
assert.match(lines[0], /เธอได้รางวัลอะไรบ้าง\?/);
|
|
186
|
+
|
|
187
|
+
agent.conversationHistory = [{ role: 'system', content: 'sys' }];
|
|
188
|
+
agent._pushRestoredToolContext();
|
|
189
|
+
assert.equal(agent.conversationHistory.length, 2);
|
|
190
|
+
assert.match(agent.conversationHistory[1].content, /RESTORED SESSION TOOL CONTEXT/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('estimateTokenCount accounts for native tool_calls payloads', () => {
|
|
194
|
+
const withCalls = estimateTokenCount([
|
|
195
|
+
{ role: 'assistant', content: '', tool_calls: [{ function: { name: 'x', arguments: 'a'.repeat(350) } }] },
|
|
196
|
+
]);
|
|
197
|
+
const withoutCalls = estimateTokenCount([{ role: 'assistant', content: '' }]);
|
|
198
|
+
assert.ok(withCalls > withoutCalls);
|
|
199
|
+
assert.ok(withCalls >= 100);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('Thai and CJK text is no longer under-counted by the flat chars/3.5 rule', () => {
|
|
203
|
+
const thai = 'จวีจิ้งอีแสดงเป็นหยางไฉ่เวยในเรื่อง In Blossom ปี สองพันยี่สิบสี่ ตัวละครมีความลึกล้ำและซับซ้อนมาก';
|
|
204
|
+
const asciiEquivalent = 'x'.repeat(thai.length);
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
assert.ok(estimateTextTokens(thai) > estimateTextTokens(asciiEquivalent) * 2);
|
|
208
|
+
assert.ok(estimateTokenCount([{ role: 'user', content: thai }]) > thai.length / 7);
|
|
209
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
detectConversationDomain,
|
|
6
|
+
resolveConversationContext
|
|
7
|
+
} from '../../web-chat/core/conversationContext.js';
|
|
8
|
+
import { applyFutureEventGuard, findFutureDates } from '../../web-chat/core/futureEventGuard.js';
|
|
9
|
+
import { executeDirectCatalogKnowledge } from '../../web-chat/core/directCatalogEngine.js';
|
|
10
|
+
|
|
11
|
+
test('ambiguous Thai follow-up inherits the Vogue Gala topic instead of the artist default', () => {
|
|
12
|
+
const context = resolveConversationContext('ทำไมน่าจับตามอง?', [
|
|
13
|
+
{ role: 'user', text: 'เล่าเรื่อง Vogue Gala 2026 ให้ฟังหน่อย' },
|
|
14
|
+
{ role: 'bot', text: 'Vogue Gala เป็นงานแฟชั่นที่มีธีมและรายชื่อแขกรับเชิญประจำปี' }
|
|
15
|
+
], new Date('2026-09-03T00:00:00Z'));
|
|
16
|
+
|
|
17
|
+
assert.equal(context.contextual, true);
|
|
18
|
+
assert.equal(context.domain, 'fashion_event');
|
|
19
|
+
assert.match(context.resolvedQuery, /Vogue Gala 2026/);
|
|
20
|
+
assert.match(context.resolvedQuery, /Do not switch the subject/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('domain detection gives the active fashion event priority over stray drama mentions', () => {
|
|
24
|
+
assert.equal(
|
|
25
|
+
detectConversationDomain('Vogue Gala 2026 ไม่ใช่การคุยเรื่อง Legend of Yunxi'),
|
|
26
|
+
'fashion_event'
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('future-event guard marks and softens unverified invitation claims', () => {
|
|
31
|
+
const now = new Date('2026-09-03T00:00:00Z');
|
|
32
|
+
const answer = 'งานจะจัดวันที่ 23 ตุลาคม 2026 และเธอได้รับเชิญเข้าร่วมงานแน่นอน';
|
|
33
|
+
assert.equal(findFutureDates(answer, now).length > 0, true);
|
|
34
|
+
|
|
35
|
+
const guarded = applyFutureEventGuard(answer, { now, tools: [] });
|
|
36
|
+
assert.match(guarded, /สถานะข้อมูลล่วงหน้า/);
|
|
37
|
+
assert.doesNotMatch(guarded, /ได้รับเชิญเข้าร่วมงานแน่นอน/);
|
|
38
|
+
assert.doesNotMatch(guarded, /23 ตุลาคม 2026/);
|
|
39
|
+
assert.match(guarded, /วันที่ในอนาคตที่ยังไม่ยืนยัน/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('direct catalog fallback preserves an inherited event topic', async () => {
|
|
43
|
+
const conversationContext = resolveConversationContext('ทำไมน่าจับตามอง?', [
|
|
44
|
+
{ role: 'user', text: 'Vogue Gala 2026 มีอะไรน่าสนใจ' },
|
|
45
|
+
{ role: 'bot', text: 'ประเด็นหลักคือธีมของงานแฟชั่นและกำหนดการ' }
|
|
46
|
+
]);
|
|
47
|
+
const result = await executeDirectCatalogKnowledge('ทำไมน่าจับตามอง?', 'default', { conversationContext });
|
|
48
|
+
|
|
49
|
+
assert.match(result.answer, /อ้างอิงหัวข้อเดิม/);
|
|
50
|
+
assert.doesNotMatch(result.answer, /ข้อมูลสรุป จวีจิ้งอี/);
|
|
51
|
+
assert.doesNotMatch(result.answer, /หยุนซี|Yunxi/i);
|
|
52
|
+
assert.equal(result.tools.length, 0);
|
|
53
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { getCpuMetrics, resolveOptimalThreadBudget, clearCpuMetricsCache } from '../cpuMetrics.js';
|
|
4
|
+
import { resolveParallelSlotBudget, buildLmsLoadArgs } from '../lmStudioWakeup.js';
|
|
5
|
+
import { normalizeModelLoadSettings } from '../config.js';
|
|
6
|
+
|
|
7
|
+
test('getCpuMetrics detects CPU topology and returns structured hardware metrics', async () => {
|
|
8
|
+
clearCpuMetricsCache();
|
|
9
|
+
const metrics = await getCpuMetrics();
|
|
10
|
+
assert.ok(metrics, 'Metrics object must be defined');
|
|
11
|
+
assert.ok(typeof metrics.model === 'string' && metrics.model.length > 0, 'CPU model must be non-empty string');
|
|
12
|
+
assert.ok(Number.isFinite(metrics.physicalCores) && metrics.physicalCores >= 1, 'Physical cores must be >= 1');
|
|
13
|
+
assert.ok(Number.isFinite(metrics.logicalCores) && metrics.logicalCores >= metrics.physicalCores, 'Logical cores must be >= physical cores');
|
|
14
|
+
assert.equal(typeof metrics.hyperThreading, 'boolean');
|
|
15
|
+
assert.ok(Number.isFinite(metrics.smtMultiplier) && metrics.smtMultiplier >= 1);
|
|
16
|
+
assert.ok(typeof metrics.statusText === 'string');
|
|
17
|
+
assert.ok(metrics.recommendedComputeThreads >= 1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('resolveOptimalThreadBudget calculates thread counts for physical, smt, auto, and custom modes', () => {
|
|
21
|
+
const mockCpu = {
|
|
22
|
+
physicalCores: 6,
|
|
23
|
+
logicalCores: 12,
|
|
24
|
+
hyperThreading: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const phys = resolveOptimalThreadBudget({ mode: 'physical', cpuInfo: mockCpu });
|
|
28
|
+
assert.equal(phys.threads, 6);
|
|
29
|
+
assert.equal(phys.mode, 'physical');
|
|
30
|
+
assert.match(phys.explanation, /physical cores/i);
|
|
31
|
+
|
|
32
|
+
const smt = resolveOptimalThreadBudget({ mode: 'smt', cpuInfo: mockCpu });
|
|
33
|
+
assert.equal(smt.threads, 12);
|
|
34
|
+
assert.equal(smt.mode, 'smt');
|
|
35
|
+
assert.match(smt.explanation, /logical threads/i);
|
|
36
|
+
|
|
37
|
+
const autoDefault = resolveOptimalThreadBudget({ mode: 'auto', cpuInfo: mockCpu });
|
|
38
|
+
assert.equal(autoDefault.threads, 6);
|
|
39
|
+
assert.equal(autoDefault.mode, 'auto');
|
|
40
|
+
|
|
41
|
+
const autoWithReq = resolveOptimalThreadBudget({ mode: 'auto', requested: 8, cpuInfo: mockCpu });
|
|
42
|
+
assert.equal(autoWithReq.threads, 8);
|
|
43
|
+
assert.equal(autoWithReq.mode, 'custom');
|
|
44
|
+
|
|
45
|
+
const custom = resolveOptimalThreadBudget({ mode: 'custom', requested: 4, cpuInfo: mockCpu });
|
|
46
|
+
assert.equal(custom.threads, 4);
|
|
47
|
+
assert.equal(custom.mode, 'custom');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('resolveParallelSlotBudget enforces physical core ceiling when htMode is physical', () => {
|
|
51
|
+
const budgetPhysical = resolveParallelSlotBudget(12, 12, { htMode: 'physical', physicalCores: 6 });
|
|
52
|
+
assert.equal(budgetPhysical.value, 6);
|
|
53
|
+
assert.equal(budgetPhysical.ceiling, 6);
|
|
54
|
+
assert.equal(budgetPhysical.capped, true);
|
|
55
|
+
|
|
56
|
+
const budgetSmt = resolveParallelSlotBudget(12, 12, { htMode: 'smt', physicalCores: 6 });
|
|
57
|
+
assert.equal(budgetSmt.value, 12);
|
|
58
|
+
assert.equal(budgetSmt.ceiling, 12);
|
|
59
|
+
assert.equal(budgetSmt.capped, false);
|
|
60
|
+
|
|
61
|
+
const budgetAuto = resolveParallelSlotBudget(8, 12, { htMode: 'auto', physicalCores: 6 });
|
|
62
|
+
assert.equal(budgetAuto.value, 8);
|
|
63
|
+
assert.equal(budgetAuto.ceiling, 12);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('buildLmsLoadArgs applies HT-aware parallel allocation flags', () => {
|
|
67
|
+
const argsPhys = buildLmsLoadArgs('test-model', {
|
|
68
|
+
cpuThreads: 12,
|
|
69
|
+
availableCpus: 12,
|
|
70
|
+
physicalCores: 6,
|
|
71
|
+
htMode: 'physical'
|
|
72
|
+
});
|
|
73
|
+
assert.ok(argsPhys.includes('--parallel'));
|
|
74
|
+
assert.equal(argsPhys[argsPhys.indexOf('--parallel') + 1], '6');
|
|
75
|
+
|
|
76
|
+
const argsSmt = buildLmsLoadArgs('test-model', {
|
|
77
|
+
cpuThreads: 12,
|
|
78
|
+
availableCpus: 12,
|
|
79
|
+
physicalCores: 6,
|
|
80
|
+
htMode: 'smt'
|
|
81
|
+
});
|
|
82
|
+
assert.ok(argsSmt.includes('--parallel'));
|
|
83
|
+
assert.equal(argsSmt[argsSmt.indexOf('--parallel') + 1], '12');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('normalizeModelLoadSettings persists and validates htMode', () => {
|
|
87
|
+
const def = normalizeModelLoadSettings({});
|
|
88
|
+
assert.equal(def.htMode, 'auto');
|
|
89
|
+
|
|
90
|
+
const phys = normalizeModelLoadSettings({ htMode: 'physical' });
|
|
91
|
+
assert.equal(phys.htMode, 'physical');
|
|
92
|
+
|
|
93
|
+
const smt = normalizeModelLoadSettings({ htMode: 'smt' });
|
|
94
|
+
assert.equal(smt.htMode, 'smt');
|
|
95
|
+
|
|
96
|
+
const allAlias = normalizeModelLoadSettings({ htMode: 'all' });
|
|
97
|
+
assert.equal(allAlias.htMode, 'smt');
|
|
98
|
+
|
|
99
|
+
const invalid = normalizeModelLoadSettings({ htMode: 'invalid_mode' });
|
|
100
|
+
assert.equal(invalid.htMode, 'auto');
|
|
101
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { executeTool } from '../tools.js';
|
|
5
|
+
import { compactToolResultForContext } from '../contextCompactor.js';
|
|
6
|
+
import { scoreCoverage } from '../eval/modules/evaluation/scoring.js';
|
|
7
|
+
import { dynamicToolRouter } from '../toolRouter.js';
|
|
8
|
+
import { buildCatalogIndex, CATEGORY_ALIASES, resolveQueryEntities } from '../catalogEntityResolver.js';
|
|
9
|
+
|
|
10
|
+
test('get_brand_endorsements resolves French high jewelry and commercial aliases', async () => {
|
|
11
|
+
const fredRes = await executeTool('get_brand_endorsements', { query: 'French high jewelry' }, { noCache: true });
|
|
12
|
+
assert.equal(fredRes.status, 'success');
|
|
13
|
+
assert.ok(fredRes.count > 0);
|
|
14
|
+
assert.ok(fredRes.brands.some(b => b.brand.toLowerCase().includes('fred')));
|
|
15
|
+
|
|
16
|
+
const tutuRes = await executeTool('get_brand_endorsements', { query: 'TuTuCare' }, { noCache: true });
|
|
17
|
+
assert.equal(tutuRes.status, 'success');
|
|
18
|
+
assert.ok(tutuRes.count > 0);
|
|
19
|
+
assert.ok(tutuRes.brands.some(b => b.brand.toLowerCase().includes('tutu') || b.brand.includes('凸凸棉')));
|
|
20
|
+
|
|
21
|
+
const lafangRes = await executeTool('get_brand_endorsements', { query: 'Lafang' }, { noCache: true });
|
|
22
|
+
assert.equal(lafangRes.status, 'success');
|
|
23
|
+
assert.ok(lafangRes.count > 0);
|
|
24
|
+
assert.ok(lafangRes.brands.some(b => b.brand.toLowerCase().includes('lafang') || b.brand.includes('拉芳')));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('search_discography and get_music_videos resolve 0.2s and Dewdrops without numeric confusion', async () => {
|
|
28
|
+
const zeroTwoRes = await executeTool('search_discography', { query: '0.2s' }, { noCache: true });
|
|
29
|
+
assert.equal(zeroTwoRes.status, 'success');
|
|
30
|
+
assert.ok(zeroTwoRes.count > 0);
|
|
31
|
+
assert.ok(zeroTwoRes.songs.some(s => s.titleEn === '0.2s' || s.titleCn === '0.2s' || s.title?.includes('0.2s')));
|
|
32
|
+
|
|
33
|
+
const dewdropsRes = await executeTool('search_discography', { query: 'Dewdrops' }, { noCache: true });
|
|
34
|
+
assert.equal(dewdropsRes.status, 'success');
|
|
35
|
+
assert.ok(dewdropsRes.count > 0);
|
|
36
|
+
assert.ok(dewdropsRes.songs.some(s => s.id?.includes('dewdrop') || s.titleEn?.toLowerCase().includes('dew') || s.title?.includes('露珠')));
|
|
37
|
+
|
|
38
|
+
const dewdropsMv = await executeTool('get_music_videos', { query: 'Dewdrops' }, { noCache: true });
|
|
39
|
+
assert.equal(dewdropsMv.status, 'success');
|
|
40
|
+
assert.ok(dewdropsMv.count > 0);
|
|
41
|
+
assert.ok(dewdropsMv.videos.some(v => v.youtubeId === 'D-vEPdasbLo' || v.titleCn?.includes('露珠')));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('compactToolResultForContext projects essential keys and eliminates decorative URLs', () => {
|
|
45
|
+
const brandResult = {
|
|
46
|
+
status: 'success',
|
|
47
|
+
count: 1,
|
|
48
|
+
brands: [
|
|
49
|
+
{
|
|
50
|
+
brand: 'FRED Paris (斐登珠宝)',
|
|
51
|
+
brandNameEn: 'FRED Paris',
|
|
52
|
+
brandNameCn: '斐登珠宝',
|
|
53
|
+
year: 2025,
|
|
54
|
+
title: 'China Brand Ambassador',
|
|
55
|
+
category: 'French High Jewelry',
|
|
56
|
+
isGlobal: false,
|
|
57
|
+
highlight: 'Force 10 High Jewelry editorial for PORT Magazine',
|
|
58
|
+
tier: 'LVMH High Jewelry',
|
|
59
|
+
youtubeCommercialSearch: 'https://youtube.com/results?search_query=FRED',
|
|
60
|
+
bilibiliSearchUrl: 'https://search.bilibili.com/all?keyword=FRED'
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const compacted = compactToolResultForContext(brandResult, { maxTokens: 800 });
|
|
66
|
+
assert.equal(compacted.brands.length, 1);
|
|
67
|
+
const b = compacted.brands[0];
|
|
68
|
+
assert.equal(b.brandNameEn, 'FRED Paris');
|
|
69
|
+
assert.equal(b.year, 2025);
|
|
70
|
+
assert.equal(b.youtubeCommercialSearch, undefined);
|
|
71
|
+
assert.equal(b.bilibiliSearchUrl, undefined);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('compactToolResultForContext preserves image evidence links and relevance scores', () => {
|
|
75
|
+
const longImageUrl = `https://example.com/images/${'a'.repeat(300)}.jpg`;
|
|
76
|
+
const imageResult = {
|
|
77
|
+
status: 'success',
|
|
78
|
+
query: '鞠婧祎 万花世界 路透',
|
|
79
|
+
normalizedQuery: '鞠婧祎 万花世界 电视剧 路透',
|
|
80
|
+
count: 1,
|
|
81
|
+
results: [
|
|
82
|
+
{
|
|
83
|
+
title: '鞠婧祎 万花世界 路透照',
|
|
84
|
+
snippet: 'on-set stills from 万花世界 filming',
|
|
85
|
+
url: 'https://example.com/article',
|
|
86
|
+
source: 'Baidu Images',
|
|
87
|
+
imageUrl: longImageUrl,
|
|
88
|
+
thumbUrl: longImageUrl,
|
|
89
|
+
sourcePageUrl: longImageUrl,
|
|
90
|
+
width: 1080,
|
|
91
|
+
height: 1440,
|
|
92
|
+
relevanceScore: 42.5,
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const compacted = compactToolResultForContext(imageResult, { maxTokens: 60 });
|
|
98
|
+
assert.equal(compacted.results.length, 1);
|
|
99
|
+
const item = compacted.results[0];
|
|
100
|
+
assert.equal(item.imageUrl, longImageUrl);
|
|
101
|
+
assert.equal(item.thumbUrl, longImageUrl);
|
|
102
|
+
assert.equal(item.sourcePageUrl, longImageUrl);
|
|
103
|
+
assert.equal(item.relevanceScore, 42.5);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('scoreCoverage normalizes Buddhist Era years and minute-second durations', () => {
|
|
107
|
+
const caseBe = {
|
|
108
|
+
groups: [
|
|
109
|
+
['2017', 'ค.ศ. 2017']
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
const answerBe = 'จวีจิ้งอีจัดงานคอนเสิร์ตเดี่ยวครั้งสำคัญใน พ.ศ. 2560 อย่างยิ่งใหญ่';
|
|
113
|
+
const scoreBeResult = scoreCoverage(caseBe, answerBe);
|
|
114
|
+
assert.equal(scoreBeResult.allMatched, true, 'พ.ศ. 2560 should cover 2017');
|
|
115
|
+
|
|
116
|
+
const caseDuration = {
|
|
117
|
+
groups: [
|
|
118
|
+
['5:01', '05:01']
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
const answerDuration = 'เพลง Still Searching (仍寻) มีความยาว 5 minutes and 1 second พอดี';
|
|
122
|
+
const scoreDurationResult = scoreCoverage(caseDuration, answerDuration);
|
|
123
|
+
assert.equal(scoreDurationResult.allMatched, true, '5 minutes and 1 second should cover 5:01');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('tool router routes timeline era queries to get_career_timeline', () => {
|
|
127
|
+
const tools = dynamicToolRouter.routeToolsForQuery('Ju Jingyi career journey and variety show milestones in 2015-2016');
|
|
128
|
+
const toolNames = tools.map(t => t.function?.name || t.name);
|
|
129
|
+
assert.ok(toolNames.includes('get_career_timeline'), 'should route to get_career_timeline');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('refusal regex intercepts premature commercial secret and mindset refusals', () => {
|
|
133
|
+
const refusalRegex = /(ไม่มี(การ)?บันทึก|ไม่มี(อยู่ใน)?ระบบ|ไม่มีข้อมูล|ไม่มีรายละเอียด|ไม่พบ|ข้อมูลทางธุรกิจ|ข้อมูลธุรกิจ|ข้อมูลเชิงพาณิชย์|ความลับทางการค้า|ความลับ|ความลับเชิงพาณิชย์|ปรัชญาการทำงาน|ตัวตนของฉัน|นอกเหนือขอบเขต|ไม่สามารถระบุ|ไม่ได้ระบุ|not found|no verified record|no record|could not find|unable to find|outside the scope|confidential|internal business|trade secret|business secret|commercial secret|proprietary|internal statistics?|operational philosophy|my own mindset|without querying|没有找到|未找到|暂无|没有记录|无法查询|商业机密|内部数据|机密)/i;
|
|
134
|
+
|
|
135
|
+
assert.ok(refusalRegex.test('ข้อมูลยอดขายของ TuTuCare เป็นความลับเชิงพาณิชย์'));
|
|
136
|
+
assert.ok(refusalRegex.test('This is confidential commercial secret information.'));
|
|
137
|
+
assert.ok(refusalRegex.test('This is internal business statistics not publicly shared.'));
|
|
138
|
+
assert.ok(refusalRegex.test('I do not have access to internal trade secrets.'));
|
|
139
|
+
assert.ok(refusalRegex.test('นี่เป็นปรัชญาการทำงานและตัวตนของฉันเอง'));
|
|
140
|
+
assert.ok(refusalRegex.test('This is outside the scope of public records.'));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('resolveQueryEntities detects timeline and brand intents with recommendedTools', () => {
|
|
144
|
+
const timelineQuery = resolveQueryEntities('ในปี 2018 ละครเรื่องอะไรที่จวีจิ้งอีแสดงร่วมกับ ไมค์ พิรัชต์');
|
|
145
|
+
assert.equal(timelineQuery.isTimelineIntent, true);
|
|
146
|
+
assert.ok(timelineQuery.recommendedTools.includes('get_career_timeline'));
|
|
147
|
+
|
|
148
|
+
const brandQuery = resolveQueryEntities('ยอดขายกล่องของขวัญ TuTuCare ของจวีจิ้งอีเป็นอย่างไร');
|
|
149
|
+
assert.equal(brandQuery.isBrandIntent, true);
|
|
150
|
+
assert.ok(brandQuery.recommendedTools.includes('get_brand_endorsements'));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('compactToolResultForContext preserves salesHighlights in brands data', () => {
|
|
154
|
+
const brandPayload = {
|
|
155
|
+
brands: [
|
|
156
|
+
{ id: 'tutucare', brand: 'TuTuCare', brandNameEn: 'TuTuCare', year: 2026, title: 'Global Ambassador' }
|
|
157
|
+
],
|
|
158
|
+
salesHighlights: [
|
|
159
|
+
{ brand: 'TuTuCare (凸凸棉)', brandId: 'tutucare', stat: '20,000 ชุด หมดใน 1 วินาที!', revenue: 'GMV เกิน 10.3 ล้านหยวนใน 1 ชั่วโมง', detail: 'กล่องของขวัญลิมิเต็ด' }
|
|
160
|
+
]
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const compacted = compactToolResultForContext(brandPayload);
|
|
164
|
+
assert.ok(compacted.salesHighlights, 'salesHighlights should be preserved');
|
|
165
|
+
assert.equal(compacted.salesHighlights.length, 1);
|
|
166
|
+
assert.equal(compacted.salesHighlights[0].stat, '20,000 ชุด หมดใน 1 วินาที!');
|
|
167
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { executeTool } from '../tools.js';
|
|
5
|
+
import { resolveConversationContext } from '../../web-chat/core/conversationContext.js';
|
|
6
|
+
import { executeDirectCatalogKnowledge } from '../../web-chat/core/directCatalogEngine.js';
|
|
7
|
+
import { assessDramaAnswerQuality } from '../../web-chat/core/dramaKnowledge.js';
|
|
8
|
+
|
|
9
|
+
test('2024-versus-past analysis compares concrete role demands without unsupported rankings', async () => {
|
|
10
|
+
const query = 'ดำเนินการต่อ: อยากให้วิเคราะห์เปรียบเทียบบทบาทของเธอในซีรีส์ยุคใหม่ (2024) กับผลงานไอดอลในอดีต ว่ามีจุดเปลี่ยนด้านการแสดงอย่างไรบ้างคะ?';
|
|
11
|
+
const result = await executeDirectCatalogKnowledge(query);
|
|
12
|
+
|
|
13
|
+
assert.match(result.answer, /วิวัฒนาการต่อเนื่อง/);
|
|
14
|
+
assert.match(result.answer, /ตัวตนหยางไฉ่เวย.*รูปลักษณ์.*ซ่างกวนจื่อ/s);
|
|
15
|
+
assert.match(result.answer, /Sword and Fairy 4/);
|
|
16
|
+
assert.match(result.answer, /Novoland: The Castle in the Sky/);
|
|
17
|
+
assert.doesNotMatch(result.answer, /★\s*9\.|ระดับแนวหน้า|ไม่มีใครเทียบได้/);
|
|
18
|
+
assert.doesNotMatch(result.answer, /ประวัติและข้อมูลส่วนตัว/);
|
|
19
|
+
assert.ok(result.tools.every(tool => tool.name === 'get_work_detail'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('White Snake story keeps Bai Suzhen and Xu Xian species and genders correct', async () => {
|
|
23
|
+
const result = await executeDirectCatalogKnowledge('เล่าเรื่องราวและเกร็ดของ ตำนานนางพญางูขาว (The Legend of White Snake) ให้ฟังหน่อย');
|
|
24
|
+
|
|
25
|
+
assert.match(result.answer, /ไป๋ซู่เจินเป็นวิญญาณ\/ปีศาจงูขาว/);
|
|
26
|
+
assert.match(result.answer, /สวี่เซียนเป็นมนุษย์ชาย/);
|
|
27
|
+
assert.match(result.answer, /iQIYI \/ WeTV/);
|
|
28
|
+
assert.doesNotMatch(result.answer, /มนุษย์สาวผู้มีความงดงาม.*พญางูขาว/s);
|
|
29
|
+
assert.doesNotMatch(result.answer, /ไม่มีใครเทียบได้|ระดับแนวหน้า/);
|
|
30
|
+
assert.equal(result.tools[0]?.name, 'get_work_detail');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('explicit long continuation keeps the previous topic and forbids repetition', () => {
|
|
34
|
+
const query = 'ดำเนินการต่อ: อยากให้วิเคราะห์เปรียบเทียบบทบาทของเธอในซีรีส์ยุคใหม่ปี 2024 กับผลงานไอดอลในอดีต โดยเน้นเฉพาะสิ่งที่ต่างจากคำตอบก่อนหน้า';
|
|
35
|
+
const context = resolveConversationContext(query, [
|
|
36
|
+
{ role: 'user', text: 'ช่วยวิเคราะห์ซีรีส์ปี 2024 กับอดีตของจวีจิ้งอี' },
|
|
37
|
+
{ role: 'assistant', text: 'คำตอบก่อนหน้ากล่าวถึง In Blossom และ The Legend of White Snake' }
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
assert.equal(context.contextual, true);
|
|
41
|
+
assert.match(context.resolvedQuery, /do not copy or restate/i);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('search_works exposes synopsis and highlights needed for grounded synthesis', async () => {
|
|
45
|
+
const result = await executeTool('search_works', { query: 'The Legend of White Snake' });
|
|
46
|
+
const work = result.works[0];
|
|
47
|
+
|
|
48
|
+
assert.match(work.synopsis, /ปีศาจงูขาว/);
|
|
49
|
+
assert.ok(Array.isArray(work.highlights));
|
|
50
|
+
assert.ok(work.highlights.length > 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('quality assessment rejects the observed hallucinated White Snake answer', () => {
|
|
54
|
+
const answer = 'ตัวละครเอกเป็นมนุษย์สาวผู้มีความงดงาม และเธอได้พบกับสิ่งมีชีวิตลึกลับ ซึ่งในตำนานคือพญางูขาว';
|
|
55
|
+
const assessment = assessDramaAnswerQuality('เล่าเรื่องราวและเกร็ดของ The Legend of White Snake', answer);
|
|
56
|
+
|
|
57
|
+
assert.equal(assessment.applicable, true);
|
|
58
|
+
assert.equal(assessment.valid, false);
|
|
59
|
+
assert.ok(assessment.issues.includes('reversed-species-or-gender'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('quality assessment rejects repeated generic comparison and false dual-role framing', () => {
|
|
63
|
+
const answer = 'In Blossom แสดงพัฒนาการสลับเล่น 2 คาแรคเตอร์ได้อย่างน่าทึ่ง ส่วน Sword and Fairy 4 เป็นงานปี 2024 ★ 9.5 / 10';
|
|
64
|
+
const assessment = assessDramaAnswerQuality(
|
|
65
|
+
'ดำเนินการต่อ: เปรียบเทียบซีรีส์ปี 2024 กับผลงานไอดอลในอดีต',
|
|
66
|
+
answer
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
assert.equal(assessment.valid, false);
|
|
70
|
+
assert.ok(assessment.issues.includes('false-dual-role-framing'));
|
|
71
|
+
assert.ok(assessment.issues.includes('unattributed-rating'));
|
|
72
|
+
assert.ok(assessment.issues.includes('missing-past-work-evidence'));
|
|
73
|
+
});
|