@wabot-dev/framework 0.9.27 → 2.0.0-beta.0
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 +27 -0
- package/bin/skills.mjs +151 -0
- package/bin/wabot-skills.mjs +120 -0
- package/dist/build/build.js +1031 -8
- package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
- package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
- package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
- package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
- package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
- package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
- package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
- package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
- package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
- package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
- package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
- package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
- package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
- package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
- package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
- package/dist/src/addon/ui/preact/outlet.js +22 -0
- package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
- package/dist/src/core/repository/CrudRepository.js +7 -7
- package/dist/src/feature/async/computeDedupKey.js +1 -1
- package/dist/src/feature/chat-controller/runChatControllers.js +4 -1
- package/dist/src/feature/pg/@pgExtension.js +2 -4
- package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
- package/dist/src/feature/project-runner/scanner.js +1 -1
- package/dist/src/feature/repository/@memExtension.js +1 -2
- package/dist/src/feature/rest-controller/runRestControllers.js +11 -6
- package/dist/src/feature/ui-controller/actions.js +35 -0
- package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
- package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
- package/dist/src/feature/ui-controller/bundler/index.js +4 -0
- package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
- package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
- package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
- package/dist/src/feature/ui-controller/document/escape.js +17 -0
- package/dist/src/feature/ui-controller/document/helpers.js +13 -0
- package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
- package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
- package/dist/src/feature/ui-controller/island/island.js +40 -0
- package/dist/src/feature/ui-controller/island/serialize.js +35 -0
- package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
- package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
- package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
- package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
- package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
- package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
- package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
- package/dist/src/index.d.ts +640 -3
- package/dist/src/index.js +32 -3
- package/dist/src/testing/LlmJudge.js +93 -0
- package/dist/src/testing/MockChatAdapter.js +68 -0
- package/dist/src/testing/TestChatMemory.js +73 -0
- package/dist/src/testing/asyncHarness.js +66 -0
- package/dist/src/testing/auth.js +114 -0
- package/dist/src/testing/chatBotHarness.js +88 -0
- package/dist/src/testing/chatControllerHarness.js +94 -0
- package/dist/src/testing/conformance/chatAdapterConformanceCases.js +656 -0
- package/dist/src/testing/fixtures.js +53 -0
- package/dist/src/testing/helpers.js +42 -0
- package/dist/src/testing/index.d.ts +818 -0
- package/dist/src/testing/index.js +14 -0
- package/dist/src/testing/repositories.js +34 -0
- package/dist/src/testing/restHarness.js +127 -0
- package/dist/src/testing/testImageBase64.js +5 -0
- package/dist/src/testing/uiHarness.js +102 -0
- package/dist/src/testing/validation.js +66 -0
- package/dist/src/ui/client.js +6 -0
- package/dist/src/ui/index.d.ts +427 -0
- package/dist/src/ui/index.js +29 -0
- package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
- package/dist/src/ui/jsx-dev-runtime.js +1 -0
- package/dist/src/ui/jsx-runtime.d.ts +1 -0
- package/dist/src/ui/jsx-runtime.js +1 -0
- package/package.json +48 -11
- package/skills/wabot-async/SKILL.md +143 -0
- package/skills/wabot-auth/SKILL.md +153 -0
- package/skills/wabot-chat/SKILL.md +140 -0
- package/skills/wabot-di-config/SKILL.md +117 -0
- package/skills/wabot-framework/SKILL.md +81 -0
- package/skills/wabot-framework/references/quickstart.md +85 -0
- package/skills/wabot-mindset/SKILL.md +159 -0
- package/skills/wabot-ops/SKILL.md +151 -0
- package/skills/wabot-persistence/SKILL.md +159 -0
- package/skills/wabot-rest-socket/SKILL.md +167 -0
- package/skills/wabot-testing/SKILL.md +214 -0
- package/skills/wabot-ui/SKILL.md +201 -0
- package/skills/wabot-validation/SKILL.md +108 -0
package/dist/src/index.js
CHANGED
|
@@ -84,7 +84,7 @@ export { stripAnsweredMedia } from './feature/chat-bot/stripAnsweredMedia.js';
|
|
|
84
84
|
export { chatController } from './feature/chat-controller/metadata/controller/@chatController.js';
|
|
85
85
|
export { ControllerMetadataStore } from './feature/chat-controller/metadata/ControllerMetadataStore.js';
|
|
86
86
|
export { ChatResolver } from './feature/chat-controller/ChatResolver.js';
|
|
87
|
-
export { runChatControllers } from './feature/chat-controller/runChatControllers.js';
|
|
87
|
+
export { prepareChatContainer, runChatControllers } from './feature/chat-controller/runChatControllers.js';
|
|
88
88
|
export { ExpressProvider } from './feature/express/ExpressProvider.js';
|
|
89
89
|
export { HttpServerProvider } from './feature/http/HttpServerProvider.js';
|
|
90
90
|
export { mindset } from './feature/mindset/metadata/mindsets/@mindset.js';
|
|
@@ -120,7 +120,7 @@ export { onPut } from './feature/rest-controller/metadata/@onPut.js';
|
|
|
120
120
|
export { onDelete } from './feature/rest-controller/metadata/@onDelete.js';
|
|
121
121
|
export { restController } from './feature/rest-controller/metadata/@restController.js';
|
|
122
122
|
export { RestControllerMetadataStore } from './feature/rest-controller/metadata/RestControllerMetadataStore.js';
|
|
123
|
-
export { runRestControllers } from './feature/rest-controller/runRestControllers.js';
|
|
123
|
+
export { registerRestControllers, runRestControllers } from './feature/rest-controller/runRestControllers.js';
|
|
124
124
|
export { EXPRESS_REQ, EXPRESS_RES } from './feature/rest-controller/injection-tokens.js';
|
|
125
125
|
export { RestRequest } from './feature/rest-controller/RestRequest.js';
|
|
126
126
|
export { SocketServerConfig } from './feature/socket/SocketServerConfig.js';
|
|
@@ -130,6 +130,20 @@ export { socketController } from './feature/socket-controller/metadata/@socketCo
|
|
|
130
130
|
export { onSocketEvent } from './feature/socket-controller/metadata/@onSocketEvent.js';
|
|
131
131
|
export { SocketControllerMetadataStore } from './feature/socket-controller/metadata/SocketControllerMetadataStore.js';
|
|
132
132
|
export { runSocketControllers } from './feature/socket-controller/runSocketControllers.js';
|
|
133
|
+
export { uiController } from './feature/ui-controller/metadata/@uiController.js';
|
|
134
|
+
export { view } from './feature/ui-controller/metadata/@view.js';
|
|
135
|
+
export { action } from './feature/ui-controller/metadata/@action.js';
|
|
136
|
+
export { uiMiddleware } from './feature/ui-controller/metadata/@uiMiddleware.js';
|
|
137
|
+
export { UiControllerMetadataStore } from './feature/ui-controller/metadata/UiControllerMetadataStore.js';
|
|
138
|
+
export { UiRendererRegistry } from './feature/ui-controller/renderer/UiRendererRegistry.js';
|
|
139
|
+
export { ISLAND_MARKER, getIslandMeta, isIsland, island, setIslandId } from './feature/ui-controller/island/island.js';
|
|
140
|
+
export { deserializeProps, serializeProps } from './feature/ui-controller/island/serialize.js';
|
|
141
|
+
export { ISLAND_FILE_PATTERN, IslandRegistry, isIslandFile, toIslandId } from './feature/ui-controller/island/IslandRegistry.js';
|
|
142
|
+
export { renderDocument } from './feature/ui-controller/document/renderDocument.js';
|
|
143
|
+
export { REDIRECT_MARKER, isRedirect, redirect } from './feature/ui-controller/document/helpers.js';
|
|
144
|
+
export { escapeAttr, escapeHtml } from './feature/ui-controller/document/escape.js';
|
|
145
|
+
export { actionUrl, callAction } from './feature/ui-controller/actions.js';
|
|
146
|
+
export { registerUiControllers, runUiControllers } from './feature/ui-controller/runUiControllers.js';
|
|
133
147
|
export { PgCronJobRepository } from './addon/async/pg/PgCronJobRepository.js';
|
|
134
148
|
export { PgJobRepository } from './addon/async/pg/PgJobRepository.js';
|
|
135
149
|
export { PgTransactionAdapter } from './addon/async/pg/PgTransactionAdapter.js';
|
|
@@ -165,6 +179,7 @@ export { PgChatMemory } from './addon/chat-bot/pg/PgChatMemory.js';
|
|
|
165
179
|
export { InMemoryChatMemory } from './addon/chat-bot/in-memory/InMemoryChatMemory.js';
|
|
166
180
|
export { InMemoryChatRepository } from './addon/chat-bot/in-memory/InMemoryChatRepository.js';
|
|
167
181
|
export { WabotChatAdapter } from './addon/chat-bot/wabot/WabotChatAdapter.js';
|
|
182
|
+
export { XAIChatAdapter } from './addon/chat-bot/xai/XAIChatAdapter.js';
|
|
168
183
|
export { cmd } from './addon/chat-controller/cmd/@cmd.js';
|
|
169
184
|
export { CmdChannel, readJsonFromFile, writeJsonToFile } from './addon/chat-controller/cmd/CmdChannel.js';
|
|
170
185
|
export { CmdChannelConfig } from './addon/chat-controller/cmd/CmdChannelConfig.js';
|
|
@@ -172,6 +187,21 @@ export { CmdChannelServer } from './addon/chat-controller/cmd/CmdChannelServer.j
|
|
|
172
187
|
export { cmdChannelName } from './addon/chat-controller/cmd/cmdChannelName.js';
|
|
173
188
|
export { cmdChannelSocketPath } from './addon/chat-controller/cmd/cmdChannelSocketPath.js';
|
|
174
189
|
export { runCmdClient } from './addon/chat-controller/cmd/runCmdClient.js';
|
|
190
|
+
export { hubspotChannelName } from './addon/chat-controller/hubspot/hubspotChannelName.js';
|
|
191
|
+
export { HubSpotChannelConfig } from './addon/chat-controller/hubspot/HubSpotChannelConfig.js';
|
|
192
|
+
export { verifyHubSpotSignatureV3 } from './addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js';
|
|
193
|
+
export { downloadHubSpotAttachments } from './addon/chat-controller/hubspot/downloadHubSpotAttachments.js';
|
|
194
|
+
export { HubSpotSender } from './addon/chat-controller/hubspot/HubSpotSender.js';
|
|
195
|
+
export { HubSpotWebhookController } from './addon/chat-controller/hubspot/HubSpotWebhookController.js';
|
|
196
|
+
export { HubSpotReceiver } from './addon/chat-controller/hubspot/HubSpotReceiver.js';
|
|
197
|
+
export { HubSpotChannel } from './addon/chat-controller/hubspot/HubSpotChannel.js';
|
|
198
|
+
export { markdownToChatHtml as markdownToHubSpotHtml, markdownToChatHtml as markdownToTelegramHtml } from './addon/chat-controller/markdown/markdownToChatHtml.js';
|
|
199
|
+
export { hubspot } from './addon/chat-controller/hubspot/@hubspot.js';
|
|
200
|
+
export { slack } from './addon/chat-controller/slack/@slack.js';
|
|
201
|
+
export { SlackChannelConfig } from './addon/chat-controller/slack/SlackChannelConfig.js';
|
|
202
|
+
export { SlackChannel } from './addon/chat-controller/slack/SlackChannel.js';
|
|
203
|
+
export { slackChannelName } from './addon/chat-controller/slack/slackChannelName.js';
|
|
204
|
+
export { markdownToSlackMrkdwn } from './addon/chat-controller/slack/markdownToSlackMrkdwn.js';
|
|
175
205
|
export { socket } from './addon/chat-controller/socket/@socket.js';
|
|
176
206
|
export { SocketChannel, SocketChannelMessageFile, SocketChannelReceivedMessage } from './addon/chat-controller/socket/SocketChannel.js';
|
|
177
207
|
export { SocketChannelConfig } from './addon/chat-controller/socket/SocketChannelConfig.js';
|
|
@@ -180,7 +210,6 @@ export { telegram } from './addon/chat-controller/telegram/@telegram.js';
|
|
|
180
210
|
export { TelegramChannelConfig } from './addon/chat-controller/telegram/TelegramChannelConfig.js';
|
|
181
211
|
export { TelegramChannel } from './addon/chat-controller/telegram/TelegramChannel.js';
|
|
182
212
|
export { telegramChannelName } from './addon/chat-controller/telegram/telegramChannelName.js';
|
|
183
|
-
export { markdownToTelegramHtml } from './addon/chat-controller/telegram/markdownToTelegramHtml.js';
|
|
184
213
|
export { WhatsAppReceiverByCloudApi } from './addon/chat-controller/whatsapp/cloud-api/WhatsAppReceiverByCloudApi.js';
|
|
185
214
|
export { WhatsAppApiSender } from './addon/chat-controller/whatsapp/cloud-api/WhatsAppApiSender.js';
|
|
186
215
|
export { kapso } from './addon/chat-controller/whatsapp/kapso/@kapso.js';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const VERDICT_TOOL = {
|
|
2
|
+
language: 'english',
|
|
3
|
+
name: 'submitVerdict',
|
|
4
|
+
description: 'Submit your evaluation verdict. You MUST always call this tool exactly once; never answer with plain text.',
|
|
5
|
+
parameters: [
|
|
6
|
+
{
|
|
7
|
+
name: 'pass',
|
|
8
|
+
required: true,
|
|
9
|
+
schema: { type: 'boolean', description: 'true if the transcript satisfies the criteria' },
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'reasoning',
|
|
13
|
+
required: true,
|
|
14
|
+
schema: { type: 'string', description: 'short explanation of the verdict' },
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
const JUDGE_SYSTEM_PROMPT = `
|
|
19
|
+
You are a strict QA judge for chatbot conversations.
|
|
20
|
+
You will receive a chat transcript and evaluation criteria.
|
|
21
|
+
Evaluate whether the transcript satisfies ALL the criteria.
|
|
22
|
+
You MUST report your verdict by calling the submitVerdict tool exactly once.
|
|
23
|
+
Never reply with plain text.
|
|
24
|
+
`;
|
|
25
|
+
function renderTranscript(items) {
|
|
26
|
+
return items
|
|
27
|
+
.map((item) => {
|
|
28
|
+
if (item.type === 'humanMessage') {
|
|
29
|
+
const msg = item.humanMessage;
|
|
30
|
+
const parts = [
|
|
31
|
+
msg.text,
|
|
32
|
+
msg.images?.length ? `[${msg.images.length} image(s)]` : undefined,
|
|
33
|
+
msg.documents?.length ? `[${msg.documents.length} document(s)]` : undefined,
|
|
34
|
+
msg.object ? JSON.stringify(msg.object) : undefined,
|
|
35
|
+
].filter(Boolean);
|
|
36
|
+
return `HUMAN: ${parts.join(' ')}`;
|
|
37
|
+
}
|
|
38
|
+
if (item.type === 'botMessage') {
|
|
39
|
+
return `BOT: ${item.botMessage.text ?? JSON.stringify(item.botMessage)}`;
|
|
40
|
+
}
|
|
41
|
+
const call = item.functionCall;
|
|
42
|
+
return `TOOL CALL: ${call.name}(${call.arguments ?? '{}'}) -> ${call.result ?? '(no result)'}`;
|
|
43
|
+
})
|
|
44
|
+
.join('\n');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Grades chatbot behavior with a real LLM. Provider-agnostic: the verdict is
|
|
48
|
+
* extracted through a forced tool call instead of parsing free-form text.
|
|
49
|
+
*/
|
|
50
|
+
class LlmJudge {
|
|
51
|
+
options;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.options = options;
|
|
54
|
+
}
|
|
55
|
+
async evaluate(req) {
|
|
56
|
+
const transcript = typeof req.transcript === 'string' ? req.transcript : renderTranscript(req.transcript);
|
|
57
|
+
const { nextItems } = await this.options.adapter.nextItems({
|
|
58
|
+
models: this.options.models,
|
|
59
|
+
systemPrompt: JUDGE_SYSTEM_PROMPT,
|
|
60
|
+
tools: [VERDICT_TOOL],
|
|
61
|
+
prevItems: [
|
|
62
|
+
{
|
|
63
|
+
type: 'humanMessage',
|
|
64
|
+
humanMessage: {
|
|
65
|
+
text: `## Criteria\n${req.criteria}\n\n## Transcript\n${transcript}\n\nEvaluate now and call submitVerdict.`,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const verdictCall = nextItems.find((item) => item.type === 'functionCall' && item.functionCall.name === VERDICT_TOOL.name);
|
|
71
|
+
if (!verdictCall || verdictCall.type !== 'functionCall') {
|
|
72
|
+
const texts = nextItems
|
|
73
|
+
.map((item) => (item.type === 'botMessage' ? item.botMessage.text : null))
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
throw new Error(`LlmJudge: the judge model did not call submitVerdict. Got: ${texts.join(' | ') || JSON.stringify(nextItems)}`);
|
|
76
|
+
}
|
|
77
|
+
const args = JSON.parse(verdictCall.functionCall.arguments ?? '{}');
|
|
78
|
+
if (typeof args.pass !== 'boolean') {
|
|
79
|
+
throw new Error(`LlmJudge: invalid verdict arguments: ${verdictCall.functionCall.arguments}`);
|
|
80
|
+
}
|
|
81
|
+
return { pass: args.pass, reasoning: String(args.reasoning ?? '') };
|
|
82
|
+
}
|
|
83
|
+
/** Like evaluate(), but throws (with the judge's reasoning) when it fails. */
|
|
84
|
+
async assert(req) {
|
|
85
|
+
const verdict = await this.evaluate(req);
|
|
86
|
+
if (!verdict.pass) {
|
|
87
|
+
throw new Error(`LlmJudge: criteria not satisfied: ${req.criteria}\n${verdict.reasoning}`);
|
|
88
|
+
}
|
|
89
|
+
return verdict;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { LlmJudge, renderTranscript };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic IChatAdapter for tests: script the LLM turns with
|
|
3
|
+
* reply()/callTool()/enqueue() and assert on the recorded requests.
|
|
4
|
+
* Each queued response is consumed by one nextItems() call, so a tool
|
|
5
|
+
* call turn must be followed by another scripted turn (the ChatBot loop
|
|
6
|
+
* calls the adapter again after executing the tool).
|
|
7
|
+
*/
|
|
8
|
+
class MockChatAdapter {
|
|
9
|
+
options;
|
|
10
|
+
requests = [];
|
|
11
|
+
queue = [];
|
|
12
|
+
callIdCounter = 0;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
/** Queue a turn where the bot answers with a plain text message. */
|
|
17
|
+
reply(text) {
|
|
18
|
+
return this.enqueue([{ type: 'botMessage', botMessage: { text } }]);
|
|
19
|
+
}
|
|
20
|
+
/** Queue a turn where the bot requests a tool call (executed for real by the ChatBot). */
|
|
21
|
+
callTool(name, args) {
|
|
22
|
+
return this.enqueue([
|
|
23
|
+
{
|
|
24
|
+
type: 'functionCall',
|
|
25
|
+
functionCall: {
|
|
26
|
+
id: `mock-call-${++this.callIdCounter}`,
|
|
27
|
+
name,
|
|
28
|
+
arguments: typeof args === 'string' ? args : JSON.stringify(args ?? {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
/** Queue a raw turn: a list of chat items, or a function of the request. */
|
|
34
|
+
enqueue(response) {
|
|
35
|
+
this.queue.push(response);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
get lastRequest() {
|
|
39
|
+
return this.requests[this.requests.length - 1];
|
|
40
|
+
}
|
|
41
|
+
pendingResponses() {
|
|
42
|
+
return this.queue.length;
|
|
43
|
+
}
|
|
44
|
+
async nextItems(req) {
|
|
45
|
+
this.requests.push(req);
|
|
46
|
+
let response = this.queue.shift();
|
|
47
|
+
if (!response) {
|
|
48
|
+
if (this.options.fallbackReply === undefined) {
|
|
49
|
+
throw new Error('MockChatAdapter: no scripted response left for this turn. ' +
|
|
50
|
+
'Queue one with reply()/callTool()/enqueue(), or set the fallbackReply option. ' +
|
|
51
|
+
'Remember that after a callTool() turn the ChatBot calls the adapter again.');
|
|
52
|
+
}
|
|
53
|
+
response = [{ type: 'botMessage', botMessage: { text: this.options.fallbackReply } }];
|
|
54
|
+
}
|
|
55
|
+
const nextItems = typeof response === 'function' ? await response(req) : response;
|
|
56
|
+
return {
|
|
57
|
+
nextItems,
|
|
58
|
+
usage: {
|
|
59
|
+
inputTokens: 1,
|
|
60
|
+
outputTokens: 1,
|
|
61
|
+
provider: 'mock',
|
|
62
|
+
model: req.models[0]?.model ?? 'mock-model',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { MockChatAdapter };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import '../feature/chat-bot/ChatAdapterRegistry.js';
|
|
3
|
+
import '../feature/chat-bot/ChatBot.js';
|
|
4
|
+
import { ChatOperator } from '../feature/chat-bot/ChatOperator.js';
|
|
5
|
+
import '../feature/chat-bot/UnionChatAdapter.js';
|
|
6
|
+
import '../core/injection/index.js';
|
|
7
|
+
import '../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
|
|
8
|
+
import 'uuid';
|
|
9
|
+
import '../feature/chat-bot/metadata/ChatBotMetadataStore.js';
|
|
10
|
+
import '../core/error/setupErrorHandlers.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pure in-RAM chat memory for tests. Unlike the in-memory addon, it never
|
|
14
|
+
* touches the filesystem and exposes the full item list for assertions.
|
|
15
|
+
*/
|
|
16
|
+
class TestChatMemory {
|
|
17
|
+
items = [];
|
|
18
|
+
async findLastItems(count) {
|
|
19
|
+
return this.items.slice(-count);
|
|
20
|
+
}
|
|
21
|
+
async create(item) {
|
|
22
|
+
if (!item.wasCreated()) {
|
|
23
|
+
item['data'].id = randomUUID();
|
|
24
|
+
item['data'].createdAt = Date.now();
|
|
25
|
+
}
|
|
26
|
+
this.items.push(item);
|
|
27
|
+
}
|
|
28
|
+
all() {
|
|
29
|
+
return [...this.items];
|
|
30
|
+
}
|
|
31
|
+
allData() {
|
|
32
|
+
return this.items.map((item) => item.getData());
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.items = [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Pure in-RAM chat repository for tests (no `.wabot/` persistence). */
|
|
39
|
+
class TestChatRepository {
|
|
40
|
+
chats = [];
|
|
41
|
+
memories = new Map();
|
|
42
|
+
async create(chat) {
|
|
43
|
+
if (chat.wasCreated()) {
|
|
44
|
+
throw new Error('Chat already created');
|
|
45
|
+
}
|
|
46
|
+
chat['data'].id = randomUUID();
|
|
47
|
+
chat['data'].createdAt = Date.now();
|
|
48
|
+
chat.validate();
|
|
49
|
+
this.chats.push(chat);
|
|
50
|
+
this.memories.set(chat.id, new TestChatMemory());
|
|
51
|
+
}
|
|
52
|
+
async update(chat) {
|
|
53
|
+
if (!chat.wasCreated()) {
|
|
54
|
+
throw new Error('Chat was not created');
|
|
55
|
+
}
|
|
56
|
+
chat.validate();
|
|
57
|
+
}
|
|
58
|
+
async findByConnection(query) {
|
|
59
|
+
return this.chats.find((chat) => chat.hasConnection(query)) ?? null;
|
|
60
|
+
}
|
|
61
|
+
async findMemory(chatId) {
|
|
62
|
+
return this.memories.get(chatId) ?? null;
|
|
63
|
+
}
|
|
64
|
+
async findOperator(chatId) {
|
|
65
|
+
const chat = this.chats.find((item) => item.id === chatId) ?? null;
|
|
66
|
+
const memory = this.memories.get(chatId);
|
|
67
|
+
if (!chat || !memory)
|
|
68
|
+
return null;
|
|
69
|
+
return new ChatOperator(chat, memory, this);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { TestChatMemory, TestChatRepository };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Auth } from '../core/auth/Auth.js';
|
|
2
|
+
import { container } from '../core/injection/index.js';
|
|
3
|
+
import { AsyncMetadataStore } from '../feature/async/AsyncMetadataStore.js';
|
|
4
|
+
import '../feature/async/TransactionMetadataStore.js';
|
|
5
|
+
import '../feature/async/Async.js';
|
|
6
|
+
import '../_virtual/index.js';
|
|
7
|
+
import '../core/error/setupErrorHandlers.js';
|
|
8
|
+
import 'node:crypto';
|
|
9
|
+
import '../feature/async/JobRepository.js';
|
|
10
|
+
import '../feature/async/JobRunner.js';
|
|
11
|
+
import '../feature/async/JobScheduler.js';
|
|
12
|
+
import '../feature/async/JobWatchdog.js';
|
|
13
|
+
import '../feature/async/CronScheduler.js';
|
|
14
|
+
import { assertValid } from './validation.js';
|
|
15
|
+
import { Container } from '../core/injection/Container.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Executes @command handlers and @cronHandler jobs inline — no PostgreSQL,
|
|
19
|
+
* no polling workers — with the same validation production applies.
|
|
20
|
+
*/
|
|
21
|
+
class AsyncHarness {
|
|
22
|
+
container;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
const child = container.createChildContainer();
|
|
25
|
+
child.register(Container, { useValue: child });
|
|
26
|
+
for (const [token, instance] of options.register ?? []) {
|
|
27
|
+
child.registerInstance(token, instance);
|
|
28
|
+
}
|
|
29
|
+
if (options.authInfo) {
|
|
30
|
+
const auth = child.resolve(Auth);
|
|
31
|
+
auth.assign(options.authInfo);
|
|
32
|
+
}
|
|
33
|
+
this.container = child;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate the command data and run its handler immediately, like the
|
|
37
|
+
* JobRunner does for a scheduled job. Returns the transformed command.
|
|
38
|
+
*/
|
|
39
|
+
async execute(commandConstructor, data = {}) {
|
|
40
|
+
const store = container.resolve(AsyncMetadataStore);
|
|
41
|
+
const commandName = store.getCommandName(commandConstructor);
|
|
42
|
+
if (!commandName) {
|
|
43
|
+
throw new Error(`AsyncHarness: ${commandConstructor.name} is not registered with the @command decorator`);
|
|
44
|
+
}
|
|
45
|
+
const handlerConstructor = store.getHandlerForCommandName(commandName);
|
|
46
|
+
if (!handlerConstructor) {
|
|
47
|
+
throw new Error(`AsyncHarness: no @commandHandler registered for command '${commandName}'`);
|
|
48
|
+
}
|
|
49
|
+
const command = assertValid(commandConstructor, data);
|
|
50
|
+
const handler = this.container.resolve(handlerConstructor);
|
|
51
|
+
await handler.handle(command);
|
|
52
|
+
return command;
|
|
53
|
+
}
|
|
54
|
+
/** Run a @cronHandler's handle() once, immediately. */
|
|
55
|
+
async runCron(cronConstructor) {
|
|
56
|
+
const store = container.resolve(AsyncMetadataStore);
|
|
57
|
+
store.requireCronMetadata(cronConstructor);
|
|
58
|
+
const handler = this.container.resolve(cronConstructor);
|
|
59
|
+
await handler.handle();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function createAsyncHarness(options = {}) {
|
|
63
|
+
return new AsyncHarness(options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { AsyncHarness, createAsyncHarness };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import '../feature/socket-controller/metadata/SocketControllerMetadataStore.js';
|
|
3
|
+
import '../core/injection/index.js';
|
|
4
|
+
import { CustomError } from '../core/error/CustomError.js';
|
|
5
|
+
import '../core/error/setupErrorHandlers.js';
|
|
6
|
+
import 'debug';
|
|
7
|
+
import '../core/validation/metadata/ValidationMetadataStore.js';
|
|
8
|
+
import 'socket.io';
|
|
9
|
+
import '../feature/socket/SocketServerConfig.js';
|
|
10
|
+
import '../feature/socket/SocketServerProvider.js';
|
|
11
|
+
import '../addon/auth/api-key/ApiKeyHandshakeGuardMiddleware.js';
|
|
12
|
+
import '../feature/rest-controller/metadata/RestControllerMetadataStore.js';
|
|
13
|
+
import '../feature/express/ExpressProvider.js';
|
|
14
|
+
import 'express';
|
|
15
|
+
import 'node:path';
|
|
16
|
+
import 'node:http';
|
|
17
|
+
import '../addon/auth/api-key/ApiKeyGuardMiddleware.js';
|
|
18
|
+
import { ApiKey } from '../addon/auth/api-key/ApiKey.js';
|
|
19
|
+
import { ApiKeyRepository } from '../addon/auth/api-key/ApiKeyRepository.js';
|
|
20
|
+
import '../addon/auth/api-key/PgApiKeyRepository.js';
|
|
21
|
+
import '../addon/auth/jwt/JwtHandshakeGuardMiddleware.js';
|
|
22
|
+
import '../addon/auth/jwt/JwtGuardMiddleware.js';
|
|
23
|
+
import '../addon/auth/jwt/Jwt.js';
|
|
24
|
+
import '../addon/auth/jwt/JwtAccessAndRefreshTokenDto.js';
|
|
25
|
+
import { JwtConfig } from '../addon/auth/jwt/JwtConfig.js';
|
|
26
|
+
import 'node:crypto';
|
|
27
|
+
import '../addon/auth/jwt/JwtSigner.js';
|
|
28
|
+
import '../addon/auth/jwt/JwtTokenDto.js';
|
|
29
|
+
import '../addon/auth/jwt/PgJwtRefreshTokenRepository.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* JWT setup for tests: a JwtConfig with a test secret (no JWT_SECRET env var
|
|
33
|
+
* needed) plus a signer, so the real JwtGuardMiddleware validates the tokens.
|
|
34
|
+
*/
|
|
35
|
+
class TestJwt {
|
|
36
|
+
config;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
const config = Object.create(JwtConfig.prototype);
|
|
39
|
+
config.algorithm = 'HS256';
|
|
40
|
+
config.secretOrPublicKey = options.secret ?? 'wabot-test-jwt-secret';
|
|
41
|
+
config.secretOrPrivateKey = options.secret ?? 'wabot-test-jwt-secret';
|
|
42
|
+
config.accessExpirationSeconds = options.accessExpirationSeconds ?? 10 * 60;
|
|
43
|
+
config.refreshExpirationSeconds = 365 * 24 * 3600;
|
|
44
|
+
this.config = config;
|
|
45
|
+
}
|
|
46
|
+
/** Sign a valid access token carrying the given authInfo. */
|
|
47
|
+
sign(authInfo) {
|
|
48
|
+
return jwt.sign(authInfo, this.config.secretOrPrivateKey, {
|
|
49
|
+
algorithm: this.config.algorithm,
|
|
50
|
+
expiresIn: this.config.accessExpirationSeconds,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** A token signed with a different secret; useful for 401 tests. */
|
|
54
|
+
signInvalid(authInfo = {}) {
|
|
55
|
+
return jwt.sign(authInfo, `${this.config.secretOrPrivateKey}-corrupted`, {
|
|
56
|
+
algorithm: this.config.algorithm,
|
|
57
|
+
expiresIn: this.config.accessExpirationSeconds,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function setupTestJwt(options = {}) {
|
|
62
|
+
return new TestJwt(options);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* In-RAM IApiKeyRepository so the real ApiKeyGuardMiddleware can run in
|
|
66
|
+
* tests. Register it with: register: [[ApiKeyRepository, testApiKeys]].
|
|
67
|
+
*/
|
|
68
|
+
class TestApiKeyRepository extends ApiKeyRepository {
|
|
69
|
+
items = [];
|
|
70
|
+
idCounter = 0;
|
|
71
|
+
/** Create a key for the given authInfo and return its secret. */
|
|
72
|
+
async addKey(authInfo, name = 'test-key') {
|
|
73
|
+
const { secret } = await this.generate({ name, authInfo });
|
|
74
|
+
return secret;
|
|
75
|
+
}
|
|
76
|
+
async find(id) {
|
|
77
|
+
return this.items.find((item) => item.id === id) ?? null;
|
|
78
|
+
}
|
|
79
|
+
async findOrThrow(id) {
|
|
80
|
+
const item = await this.find(id);
|
|
81
|
+
if (!item) {
|
|
82
|
+
throw new CustomError({ message: `Not found ApiKey with id = '${id}'`, httpCode: 404 });
|
|
83
|
+
}
|
|
84
|
+
return item;
|
|
85
|
+
}
|
|
86
|
+
async findByMetadata(metadata) {
|
|
87
|
+
return this.items.filter((item) => Object.entries(metadata).every(([key, value]) => item.metadata[key] === value));
|
|
88
|
+
}
|
|
89
|
+
async create(item) {
|
|
90
|
+
if (!item.wasCreated()) {
|
|
91
|
+
item['data'].id = `test-api-key-${++this.idCounter}`;
|
|
92
|
+
item['data'].createdAt = Date.now();
|
|
93
|
+
}
|
|
94
|
+
this.items.push(item);
|
|
95
|
+
}
|
|
96
|
+
async generate(req) {
|
|
97
|
+
const apiKey = new ApiKey({ name: req.name, metadata: req.metadata, authInfo: req.authInfo });
|
|
98
|
+
const secret = apiKey.generateSecret();
|
|
99
|
+
await this.create(apiKey);
|
|
100
|
+
return { apiKey, secret };
|
|
101
|
+
}
|
|
102
|
+
async findBySecret(secret) {
|
|
103
|
+
return this.items.find((item) => item.isValidSecret(secret)) ?? null;
|
|
104
|
+
}
|
|
105
|
+
async findAndValidate(secret) {
|
|
106
|
+
const apiKey = await this.findBySecret(secret);
|
|
107
|
+
if (!apiKey) {
|
|
108
|
+
throw new CustomError({ message: 'Invalid API key', httpCode: 401 });
|
|
109
|
+
}
|
|
110
|
+
return apiKey.authInfo;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { TestApiKeyRepository, TestJwt, setupTestJwt };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Auth } from '../core/auth/Auth.js';
|
|
2
|
+
import { container } from '../core/injection/index.js';
|
|
3
|
+
import { ChatAdapter } from '../feature/chat-bot/ChatAdapter.js';
|
|
4
|
+
import '../feature/chat-bot/ChatAdapterRegistry.js';
|
|
5
|
+
import { ChatBot } from '../feature/chat-bot/ChatBot.js';
|
|
6
|
+
import { ChatMemory } from '../feature/chat-bot/ChatMemory.js';
|
|
7
|
+
import '../feature/chat-bot/ChatOperator.js';
|
|
8
|
+
import '../feature/chat-bot/UnionChatAdapter.js';
|
|
9
|
+
import '../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
|
|
10
|
+
import 'uuid';
|
|
11
|
+
import '../feature/chat-bot/metadata/ChatBotMetadataStore.js';
|
|
12
|
+
import '../core/error/setupErrorHandlers.js';
|
|
13
|
+
import '../feature/mindset/metadata/MindsetMetadataStore.js';
|
|
14
|
+
import { Mindset } from '../feature/mindset/IMindset.js';
|
|
15
|
+
import { MindsetOperator } from '../feature/mindset/MindsetOperator.js';
|
|
16
|
+
import { humanMessage } from './fixtures.js';
|
|
17
|
+
import { MockChatAdapter } from './MockChatAdapter.js';
|
|
18
|
+
import { TestChatMemory } from './TestChatMemory.js';
|
|
19
|
+
import { Container } from '../core/injection/Container.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Runs a real ChatBot (real MindsetOperator, system prompt, tool loop and
|
|
23
|
+
* argument validation) against an in-RAM memory and a pluggable adapter.
|
|
24
|
+
*/
|
|
25
|
+
class ChatBotHarness {
|
|
26
|
+
adapter;
|
|
27
|
+
memory = new TestChatMemory();
|
|
28
|
+
container;
|
|
29
|
+
chatBot;
|
|
30
|
+
operator;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.adapter = options.adapter ?? new MockChatAdapter();
|
|
33
|
+
const child = container.createChildContainer();
|
|
34
|
+
child.register(Container, { useValue: child });
|
|
35
|
+
child.register(Mindset, { useClass: options.mindset });
|
|
36
|
+
child.registerInstance(ChatMemory, this.memory);
|
|
37
|
+
child.registerInstance(ChatAdapter, this.adapter);
|
|
38
|
+
for (const [token, instance] of options.register ?? []) {
|
|
39
|
+
child.registerInstance(token, instance);
|
|
40
|
+
}
|
|
41
|
+
if (options.authInfo) {
|
|
42
|
+
const auth = child.resolve(Auth);
|
|
43
|
+
auth.assign(options.authInfo);
|
|
44
|
+
}
|
|
45
|
+
this.container = child;
|
|
46
|
+
this.chatBot = child.resolve(ChatBot);
|
|
47
|
+
this.operator = child.resolve(MindsetOperator);
|
|
48
|
+
}
|
|
49
|
+
/** Send a human message and collect everything the bot did in response. */
|
|
50
|
+
async send(message) {
|
|
51
|
+
const itemsBefore = this.memory.all().length;
|
|
52
|
+
const replies = [];
|
|
53
|
+
await this.chatBot.sendMessage(humanMessage(message), async (reply) => {
|
|
54
|
+
replies.push(reply);
|
|
55
|
+
});
|
|
56
|
+
const items = this.memory.allData().slice(itemsBefore);
|
|
57
|
+
const toolCalls = items
|
|
58
|
+
.filter((item) => item.type === 'functionCall')
|
|
59
|
+
.map((item) => item.functionCall);
|
|
60
|
+
return { replies, toolCalls, items };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Execute a single mindset tool directly (real argument validation and
|
|
64
|
+
* module resolution), without scripting a whole conversation.
|
|
65
|
+
* Returns the string result exactly as the LLM would receive it.
|
|
66
|
+
*/
|
|
67
|
+
async callTool(name, args = {}) {
|
|
68
|
+
const argsJson = typeof args === 'string' ? args : JSON.stringify(args);
|
|
69
|
+
return await this.operator.callFunction(name, argsJson);
|
|
70
|
+
}
|
|
71
|
+
/** Full chat history (data form) accumulated across turns. */
|
|
72
|
+
history() {
|
|
73
|
+
return this.memory.allData();
|
|
74
|
+
}
|
|
75
|
+
/** The real system prompt the mindset produces. */
|
|
76
|
+
async systemPrompt() {
|
|
77
|
+
return await this.operator.systemPrompt();
|
|
78
|
+
}
|
|
79
|
+
/** The real tool definitions derived from the mindset modules. */
|
|
80
|
+
tools() {
|
|
81
|
+
return this.operator.tools();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function createChatBotHarness(options) {
|
|
85
|
+
return new ChatBotHarness(options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { ChatBotHarness, createChatBotHarness };
|