@coffer-org/plugin-webchat 6.0.0 → 7.0.1
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/dist/{ChatWidget-DFVDexpD.js → ChatWidget-DkEKl1-2.js} +406 -113
- package/dist/runtime/actions.d.ts +17 -1
- package/dist/runtime/actions.js +72 -9
- package/dist/runtime/chain-store.d.ts +20 -4
- package/dist/runtime/chain-store.js +86 -9
- package/dist/runtime/channel-registry.d.ts +9 -0
- package/dist/runtime/channel-registry.js +48 -0
- package/dist/runtime/config.js +0 -1
- package/dist/runtime/connector.d.ts +3 -2
- package/dist/runtime/connector.js +77 -42
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +6 -3
- package/dist/runtime/send.d.ts +8 -7
- package/dist/runtime/send.js +61 -29
- package/dist/schema.js +336 -21
- package/dist/web.js +1 -1
- package/locales/en.json +3 -0
- package/locales/ru.json +3 -0
- package/locales/uk.json +3 -0
- package/package.json +3 -3
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
|
|
2
|
-
import { type AgentDescriptor } from '@coffer-org/server/orchestrator';
|
|
2
|
+
import { type AgentDescriptor, type ConnectorCapabilities } from '@coffer-org/server/orchestrator';
|
|
3
3
|
import { type ThreadSelection } from './chain-store.ts';
|
|
4
|
+
export interface StreamCtx {
|
|
5
|
+
caller: ActionCaller;
|
|
6
|
+
emit: (event: string, data: unknown) => void;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}
|
|
4
9
|
export interface ThreadSummary {
|
|
5
10
|
convId: string;
|
|
6
11
|
title: string;
|
|
7
12
|
lastTs: number;
|
|
8
13
|
count: number;
|
|
14
|
+
ownerName: string | null;
|
|
15
|
+
mine: boolean;
|
|
16
|
+
private: boolean;
|
|
9
17
|
}
|
|
10
18
|
export interface HistoryMsg {
|
|
11
19
|
msgId: string;
|
|
@@ -38,3 +46,11 @@ export declare function agentsAction(body: Record<string, unknown>, caller: Acti
|
|
|
38
46
|
export declare function selectAgentAction(body: Record<string, unknown>, caller: ActionCaller): Promise<{
|
|
39
47
|
selection: ThreadSelection;
|
|
40
48
|
}>;
|
|
49
|
+
export declare function channelAction(body: Record<string, unknown>, ctx: StreamCtx): Promise<void>;
|
|
50
|
+
export interface SetVisibilityDeps {
|
|
51
|
+
capabilities?: ConnectorCapabilities;
|
|
52
|
+
}
|
|
53
|
+
export declare function setVisibilityAction(body: Record<string, unknown>, caller: ActionCaller, deps?: SetVisibilityDeps): Promise<{
|
|
54
|
+
visibility: 'private' | null;
|
|
55
|
+
}>;
|
|
56
|
+
export declare function stopAction(body: Record<string, unknown>, caller: ActionCaller): Promise<void>;
|
package/dist/runtime/actions.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { HttpError } from '@coffer-org/server/plugin-hooks';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { SIDECAR_ROLES } from '@coffer-org/server/thread-store';
|
|
3
|
+
import { listUsers } from '@coffer-org/server/auth-store';
|
|
4
|
+
import { abortTurn } from '@coffer-org/server/turn-gate';
|
|
5
|
+
import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, getConversationStarters, mayBePrivate, mayRead, } from '@coffer-org/server/orchestrator';
|
|
6
|
+
import { openConversation, conversations, history, getSelection, setSelection, } from "./chain-store.js";
|
|
7
|
+
import { CAPABILITIES } from "./send.js";
|
|
8
|
+
import { subscribe, evictWhere } from "./channel-registry.js";
|
|
4
9
|
const TITLE_MAX = 60;
|
|
5
10
|
function parseSuggestions(text) {
|
|
6
11
|
try {
|
|
@@ -17,13 +22,17 @@ function title(firstUserText) {
|
|
|
17
22
|
}
|
|
18
23
|
export async function threadsAction(_body, caller) {
|
|
19
24
|
const chats = await conversations(caller.id);
|
|
20
|
-
const
|
|
25
|
+
const users = await listUsers();
|
|
26
|
+
const nameById = new Map(users.map((u) => [String(u.id), u.displayName]));
|
|
21
27
|
return {
|
|
22
28
|
threads: chats.map((c) => ({
|
|
23
|
-
convId: c.
|
|
24
|
-
title: title(c.firstUserText),
|
|
29
|
+
convId: c.convId,
|
|
30
|
+
title: c.title ?? title(c.firstUserText),
|
|
25
31
|
lastTs: c.lastTs,
|
|
26
32
|
count: c.count,
|
|
33
|
+
ownerName: c.owner === null ? null : (nameById.get(c.owner) ?? null),
|
|
34
|
+
mine: c.owner === caller.id,
|
|
35
|
+
private: c.visibility === 'private',
|
|
27
36
|
})),
|
|
28
37
|
};
|
|
29
38
|
}
|
|
@@ -33,15 +42,19 @@ function requireConvId(body) {
|
|
|
33
42
|
throw new HttpError(400, 'missing required field: convId');
|
|
34
43
|
return convId;
|
|
35
44
|
}
|
|
45
|
+
const EMPTY_SELECTION = { agentId: null, presetId: null, title: null, owner: null, visibility: null };
|
|
36
46
|
export async function historyAction(body, caller) {
|
|
37
47
|
const convId = requireConvId(body);
|
|
38
|
-
const
|
|
48
|
+
const chatId = await openConversation(convId, caller.id, 'read');
|
|
49
|
+
if (chatId === null)
|
|
50
|
+
return { convId, messages: [], headMsgId: null };
|
|
51
|
+
const rows = await history(chatId);
|
|
39
52
|
const reasoningByBot = new Map(rows.filter((m) => m.role === 'reasoning').map((m) => [m.msgId.slice(0, -2), m.text]));
|
|
40
53
|
const suggestionsByBot = new Map(rows
|
|
41
54
|
.filter((m) => m.role === 'suggestions')
|
|
42
55
|
.map((m) => [m.msgId.slice(0, -2), parseSuggestions(m.text)])
|
|
43
56
|
.filter((entry) => entry[1] !== null));
|
|
44
|
-
const visible = rows.filter((m) =>
|
|
57
|
+
const visible = rows.filter((m) => !SIDECAR_ROLES.includes(m.role));
|
|
45
58
|
return {
|
|
46
59
|
convId,
|
|
47
60
|
messages: visible.map((m) => ({
|
|
@@ -58,16 +71,19 @@ export async function historyAction(body, caller) {
|
|
|
58
71
|
}
|
|
59
72
|
export async function agentsAction(body, caller) {
|
|
60
73
|
const convId = requireConvId(body);
|
|
74
|
+
const chatId = await openConversation(convId, caller.id, 'read');
|
|
61
75
|
return {
|
|
62
76
|
agents: await listAgentCatalog(),
|
|
63
77
|
defaultAgentId: getDefaultAgentId() ?? null,
|
|
64
|
-
selection: await getSelection(
|
|
78
|
+
selection: chatId === null ? EMPTY_SELECTION : await getSelection(chatId),
|
|
65
79
|
starters: await getConversationStarters(),
|
|
66
80
|
};
|
|
67
81
|
}
|
|
68
82
|
export async function selectAgentAction(body, caller) {
|
|
69
83
|
const convId = requireConvId(body);
|
|
70
|
-
const chatId =
|
|
84
|
+
const chatId = await openConversation(convId, caller.id, 'write');
|
|
85
|
+
if (chatId === null)
|
|
86
|
+
return { selection: EMPTY_SELECTION };
|
|
71
87
|
const patch = {};
|
|
72
88
|
if ('agentId' in body) {
|
|
73
89
|
const agentId = typeof body['agentId'] === 'string' && body['agentId'] ? body['agentId'] : null;
|
|
@@ -80,3 +96,50 @@ export async function selectAgentAction(body, caller) {
|
|
|
80
96
|
await setSelection(chatId, patch);
|
|
81
97
|
return { selection: await getSelection(chatId) };
|
|
82
98
|
}
|
|
99
|
+
export async function channelAction(body, ctx) {
|
|
100
|
+
const convId = requireConvId(body);
|
|
101
|
+
const chatId = await openConversation(convId, ctx.caller.id, 'read');
|
|
102
|
+
if (chatId === null)
|
|
103
|
+
return;
|
|
104
|
+
let resolveHeld;
|
|
105
|
+
const held = new Promise((resolve) => {
|
|
106
|
+
resolveHeld = resolve;
|
|
107
|
+
});
|
|
108
|
+
const off = subscribe(chatId, {
|
|
109
|
+
viewerId: ctx.caller.id,
|
|
110
|
+
emit: ctx.emit,
|
|
111
|
+
close: () => resolveHeld(),
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
if (!ctx.signal.aborted) {
|
|
115
|
+
ctx.signal.addEventListener('abort', () => resolveHeld(), { once: true });
|
|
116
|
+
await held;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
off();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function setVisibilityAction(body, caller, deps = {}) {
|
|
124
|
+
const convId = requireConvId(body);
|
|
125
|
+
const makePrivate = body['private'] === true;
|
|
126
|
+
const chatId = await openConversation(convId, caller.id, 'write');
|
|
127
|
+
if (chatId === null)
|
|
128
|
+
return { visibility: null };
|
|
129
|
+
const selection = await getSelection(chatId);
|
|
130
|
+
if (selection.owner !== caller.id)
|
|
131
|
+
return { visibility: selection.visibility };
|
|
132
|
+
if (makePrivate && !mayBePrivate(deps.capabilities ?? CAPABILITIES))
|
|
133
|
+
return { visibility: selection.visibility };
|
|
134
|
+
const visibility = makePrivate ? 'private' : null;
|
|
135
|
+
await setSelection(chatId, { visibility });
|
|
136
|
+
evictWhere(chatId, (viewerId) => !mayRead({ owner: selection.owner, visibility }, viewerId));
|
|
137
|
+
return { visibility };
|
|
138
|
+
}
|
|
139
|
+
export async function stopAction(body, caller) {
|
|
140
|
+
const convId = requireConvId(body);
|
|
141
|
+
const chatId = await openConversation(convId, caller.id, 'write');
|
|
142
|
+
if (chatId === null)
|
|
143
|
+
return;
|
|
144
|
+
abortTurn('webchat', chatId);
|
|
145
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type StoredMsg } from '@coffer-org/server/thread-store';
|
|
2
2
|
import { type ThreadSelection } from '@coffer-org/server/thread-state';
|
|
3
|
-
import type { ConvMessage } from '@coffer-org/server/orchestrator';
|
|
3
|
+
import type { ConvMessage, ContextFact } from '@coffer-org/server/orchestrator';
|
|
4
4
|
export declare const CONNECTOR = "webchat";
|
|
5
5
|
export declare function chatIdFor(userId: string, convId: string): string;
|
|
6
|
-
export declare function chatPrefixFor(userId: string): string;
|
|
7
6
|
export declare function recordUser(m: {
|
|
8
7
|
chatId: string;
|
|
9
8
|
msgId: string;
|
|
@@ -34,13 +33,30 @@ export declare function recordSuggestions(m: {
|
|
|
34
33
|
suggestions: string[];
|
|
35
34
|
now: number;
|
|
36
35
|
}): Promise<boolean>;
|
|
36
|
+
export declare function recordContext(m: {
|
|
37
|
+
chatId: string;
|
|
38
|
+
userMsgId: string;
|
|
39
|
+
facts: ContextFact[];
|
|
40
|
+
ts: number;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
export declare function userTurnCount(chatId: string): Promise<number>;
|
|
37
43
|
export declare function buildChain(headMsgId: string, opts: {
|
|
38
44
|
chatId: string;
|
|
39
45
|
maxDepth?: number;
|
|
40
46
|
}): Promise<ConvMessage[]>;
|
|
41
47
|
export declare function history(chatId: string, limit?: number): Promise<StoredMsg[]>;
|
|
42
|
-
export
|
|
48
|
+
export interface ConversationSummary {
|
|
49
|
+
convId: string;
|
|
50
|
+
lastTs: number;
|
|
51
|
+
count: number;
|
|
52
|
+
firstUserText: string;
|
|
53
|
+
title: string | null;
|
|
54
|
+
owner: string | null;
|
|
55
|
+
visibility: 'private' | null;
|
|
56
|
+
}
|
|
57
|
+
export declare function conversations(viewerId: string): Promise<ConversationSummary[]>;
|
|
43
58
|
export type { ThreadSelection };
|
|
59
|
+
export declare function openConversation(convId: string, viewerId: string, need: 'read' | 'write'): Promise<string | null>;
|
|
44
60
|
export declare function getSelection(chatId: string): Promise<ThreadSelection>;
|
|
45
61
|
export declare function setSelection(chatId: string, patch: Partial<ThreadSelection>): Promise<void>;
|
|
46
62
|
export declare function readSelectionForTurn(chatId: string): Promise<ThreadSelection>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getThreadMessage, putThreadMessage, listThreadMessages,
|
|
2
|
-
import { getThreadState, setThreadState, readAndTouchThreadState, } from '@coffer-org/server/thread-state';
|
|
1
|
+
import { getThreadMessage, putThreadMessage, listThreadMessages, listAllThreadChats, countUserTurns, SIDECAR_ROLES, } from '@coffer-org/server/thread-store';
|
|
2
|
+
import { getThreadState, getThreadStates, setThreadState, readAndTouchThreadState, findChatByConvId, } from '@coffer-org/server/thread-state';
|
|
3
|
+
import { CONNECTOR_FACT_NAME, mayRead, mayWrite } from '@coffer-org/server/orchestrator';
|
|
3
4
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
4
5
|
const log = getLogger('webchat');
|
|
5
6
|
export const CONNECTOR = 'webchat';
|
|
@@ -7,8 +8,10 @@ const DEFAULT_MAX_DEPTH = 40;
|
|
|
7
8
|
export function chatIdFor(userId, convId) {
|
|
8
9
|
return `u:${userId}:${convId}`;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
function convIdOf(chatId) {
|
|
12
|
+
const first = chatId.indexOf(':');
|
|
13
|
+
const second = first === -1 ? -1 : chatId.indexOf(':', first + 1);
|
|
14
|
+
return second === -1 ? null : chatId.slice(second + 1);
|
|
12
15
|
}
|
|
13
16
|
export async function recordUser(m) {
|
|
14
17
|
await putThreadMessage({
|
|
@@ -65,17 +68,46 @@ export async function recordSuggestions(m) {
|
|
|
65
68
|
});
|
|
66
69
|
return true;
|
|
67
70
|
}
|
|
71
|
+
export async function recordContext(m) {
|
|
72
|
+
await putThreadMessage({
|
|
73
|
+
connector: CONNECTOR,
|
|
74
|
+
chatId: m.chatId,
|
|
75
|
+
msgId: `${m.userMsgId}~c`,
|
|
76
|
+
role: 'context',
|
|
77
|
+
text: JSON.stringify(m.facts),
|
|
78
|
+
ts: m.ts,
|
|
79
|
+
replyToId: null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function parseFacts(text) {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(text);
|
|
85
|
+
return Array.isArray(parsed)
|
|
86
|
+
? parsed.filter((f) => typeof f?.name === 'string' &&
|
|
87
|
+
CONNECTOR_FACT_NAME.test(f.name) &&
|
|
88
|
+
typeof f.value === 'string' &&
|
|
89
|
+
Object.entries(f.attrs ?? {}).every(([k, v]) => CONNECTOR_FACT_NAME.test(k) && typeof v === 'string'))
|
|
90
|
+
: [];
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function userTurnCount(chatId) {
|
|
97
|
+
return countUserTurns(CONNECTOR, chatId);
|
|
98
|
+
}
|
|
68
99
|
export async function buildChain(headMsgId, opts) {
|
|
69
100
|
const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
70
101
|
const acc = [];
|
|
102
|
+
let turns = 0;
|
|
71
103
|
let cur = headMsgId;
|
|
72
104
|
const seen = new Set();
|
|
73
|
-
while (cur &&
|
|
105
|
+
while (cur && turns < max && !seen.has(cur)) {
|
|
74
106
|
seen.add(cur);
|
|
75
107
|
const stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
|
|
76
108
|
if (!stored)
|
|
77
109
|
break;
|
|
78
|
-
if (
|
|
110
|
+
if (!SIDECAR_ROLES.includes(stored.role)) {
|
|
79
111
|
acc.push({
|
|
80
112
|
role: stored.role,
|
|
81
113
|
content: stored.text,
|
|
@@ -84,10 +116,17 @@ export async function buildChain(headMsgId, opts) {
|
|
|
84
116
|
msgId: stored.msgId,
|
|
85
117
|
ts: stored.ts,
|
|
86
118
|
});
|
|
119
|
+
turns += 1;
|
|
120
|
+
if (stored.role === 'user') {
|
|
121
|
+
const ctx = await getThreadMessage(CONNECTOR, opts.chatId, `${stored.msgId}~c`);
|
|
122
|
+
const facts = ctx ? parseFacts(ctx.text) : [];
|
|
123
|
+
if (facts.length)
|
|
124
|
+
acc[acc.length - 1].context = facts;
|
|
125
|
+
}
|
|
87
126
|
}
|
|
88
127
|
cur = stored.replyToId;
|
|
89
128
|
}
|
|
90
|
-
if (
|
|
129
|
+
if (turns >= max)
|
|
91
130
|
log.info(`chain capped at ${max} for ${headMsgId}`);
|
|
92
131
|
while (acc.length && acc.at(-1)?.role !== 'user')
|
|
93
132
|
acc.pop();
|
|
@@ -96,8 +135,46 @@ export async function buildChain(headMsgId, opts) {
|
|
|
96
135
|
export function history(chatId, limit) {
|
|
97
136
|
return listThreadMessages(CONNECTOR, chatId, limit);
|
|
98
137
|
}
|
|
99
|
-
export function conversations(
|
|
100
|
-
|
|
138
|
+
export async function conversations(viewerId) {
|
|
139
|
+
const chats = await listAllThreadChats(CONNECTOR);
|
|
140
|
+
const states = await getThreadStates(CONNECTOR, chats.map((c) => c.chatId));
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const c of chats) {
|
|
143
|
+
const access = states.get(c.chatId) ?? {
|
|
144
|
+
agentId: null,
|
|
145
|
+
presetId: null,
|
|
146
|
+
title: null,
|
|
147
|
+
owner: null,
|
|
148
|
+
visibility: null,
|
|
149
|
+
};
|
|
150
|
+
if (!mayRead(access, viewerId))
|
|
151
|
+
continue;
|
|
152
|
+
const convId = convIdOf(c.chatId);
|
|
153
|
+
if (convId === null)
|
|
154
|
+
continue;
|
|
155
|
+
out.push({
|
|
156
|
+
convId,
|
|
157
|
+
lastTs: c.lastTs,
|
|
158
|
+
count: c.count,
|
|
159
|
+
firstUserText: c.firstUserText,
|
|
160
|
+
title: access.title,
|
|
161
|
+
owner: access.owner,
|
|
162
|
+
visibility: access.visibility,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
export async function openConversation(convId, viewerId, need) {
|
|
168
|
+
if (!convId || convId.includes(':'))
|
|
169
|
+
return null;
|
|
170
|
+
const lookup = await findChatByConvId(CONNECTOR, convId);
|
|
171
|
+
if (lookup.found === 'ambiguous')
|
|
172
|
+
return null;
|
|
173
|
+
if (lookup.found === 'none')
|
|
174
|
+
return chatIdFor(viewerId, convId);
|
|
175
|
+
const access = await getThreadState(CONNECTOR, lookup.chatId);
|
|
176
|
+
const allowed = need === 'read' ? mayRead(access, viewerId) : mayWrite(access, viewerId);
|
|
177
|
+
return allowed ? lookup.chatId : null;
|
|
101
178
|
}
|
|
102
179
|
export async function getSelection(chatId) {
|
|
103
180
|
return getThreadState(CONNECTOR, chatId);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Subscriber {
|
|
2
|
+
viewerId: string;
|
|
3
|
+
emit(event: string, data: unknown): void;
|
|
4
|
+
close(): void;
|
|
5
|
+
}
|
|
6
|
+
export declare function subscribe(chatId: string, sub: Subscriber): () => void;
|
|
7
|
+
export declare function broadcast(chatId: string, event: string, data: unknown): void;
|
|
8
|
+
export declare function evictWhere(chatId: string, drop: (viewerId: string) => boolean): void;
|
|
9
|
+
export declare function subscriberCount(chatId: string): number;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
2
|
+
const log = getLogger('webchat');
|
|
3
|
+
const channels = new Map();
|
|
4
|
+
export function subscribe(chatId, sub) {
|
|
5
|
+
let set = channels.get(chatId);
|
|
6
|
+
if (!set) {
|
|
7
|
+
set = new Set();
|
|
8
|
+
channels.set(chatId, set);
|
|
9
|
+
}
|
|
10
|
+
set.add(sub);
|
|
11
|
+
return () => {
|
|
12
|
+
const current = channels.get(chatId);
|
|
13
|
+
if (!current)
|
|
14
|
+
return;
|
|
15
|
+
current.delete(sub);
|
|
16
|
+
if (current.size === 0)
|
|
17
|
+
channels.delete(chatId);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function broadcast(chatId, event, data) {
|
|
21
|
+
const set = channels.get(chatId);
|
|
22
|
+
if (!set)
|
|
23
|
+
return;
|
|
24
|
+
for (const sub of set) {
|
|
25
|
+
try {
|
|
26
|
+
sub.emit(event, data);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
log.warn(`channel emit failed for viewer ${sub.viewerId} on ${chatId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function evictWhere(chatId, drop) {
|
|
34
|
+
const set = channels.get(chatId);
|
|
35
|
+
if (!set)
|
|
36
|
+
return;
|
|
37
|
+
for (const sub of set) {
|
|
38
|
+
if (!drop(sub.viewerId))
|
|
39
|
+
continue;
|
|
40
|
+
set.delete(sub);
|
|
41
|
+
sub.close();
|
|
42
|
+
}
|
|
43
|
+
if (set.size === 0)
|
|
44
|
+
channels.delete(chatId);
|
|
45
|
+
}
|
|
46
|
+
export function subscriberCount(chatId) {
|
|
47
|
+
return channels.get(chatId)?.size ?? 0;
|
|
48
|
+
}
|
package/dist/runtime/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Connector } from '@coffer-org/server/orchestrator';
|
|
2
|
-
import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions } from './chain-store.ts';
|
|
2
|
+
import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, recordContext as storeContext, setSelection as storeSelection } from './chain-store.ts';
|
|
3
3
|
import type { ReasoningDisplay } from './settings.ts';
|
|
4
4
|
export interface StreamingConnector {
|
|
5
5
|
connector: Connector;
|
|
@@ -9,10 +9,11 @@ export interface StreamingConnector {
|
|
|
9
9
|
export interface StreamingConnectorOpts {
|
|
10
10
|
chatId: string;
|
|
11
11
|
botMsgId: string;
|
|
12
|
-
emit: (event: string, data: unknown) => void;
|
|
13
12
|
display: ReasoningDisplay;
|
|
14
13
|
record?: typeof storeAssistant;
|
|
15
14
|
recordReasoning?: typeof storeReasoning;
|
|
16
15
|
recordSuggestions?: typeof storeSuggestions;
|
|
16
|
+
recordContext?: typeof storeContext;
|
|
17
|
+
setSelection?: typeof storeSelection;
|
|
17
18
|
}
|
|
18
19
|
export declare function makeStreamingConnector(opts: StreamingConnectorOpts): StreamingConnector;
|
|
@@ -1,71 +1,106 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, } from "./chain-store.js";
|
|
1
|
+
import { makeLiveSink, plainRender } from '@coffer-org/server/orchestrator';
|
|
2
|
+
import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, recordContext as storeContext, setSelection as storeSelection, } from "./chain-store.js";
|
|
3
|
+
import { broadcast } from "./channel-registry.js";
|
|
4
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
5
|
+
const log = getLogger('webchat');
|
|
3
6
|
const WEBCHAT_THROTTLE_MS = 250;
|
|
4
7
|
const WEBCHAT_MAX = 100_000;
|
|
5
8
|
export function makeStreamingConnector(opts) {
|
|
6
9
|
const record = opts.record ?? storeAssistant;
|
|
7
10
|
const recordReasoningFn = opts.recordReasoning ?? storeReasoning;
|
|
8
11
|
const recordSuggestionsFn = opts.recordSuggestions ?? storeSuggestions;
|
|
12
|
+
const recordContextFn = opts.recordContext ?? storeContext;
|
|
13
|
+
const setSelectionFn = opts.setSelection ?? storeSelection;
|
|
9
14
|
let recordedId = null;
|
|
10
15
|
let lastSuggestions = null;
|
|
11
16
|
const connector = {
|
|
12
17
|
id: 'webchat',
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
enrolmentNotice: 'unreachable: a web-chat turn is always opened by an already-authenticated session, so ' +
|
|
19
|
+
'the identity gate never refuses one — this connector has no unidentified sender to say it to.',
|
|
20
|
+
open(envelope) {
|
|
21
|
+
const inner = makeLiveSink({
|
|
15
22
|
ops: {
|
|
16
23
|
send: async (text) => {
|
|
17
|
-
opts.
|
|
24
|
+
broadcast(opts.chatId, 'message', { msgId: opts.botMsgId, text });
|
|
18
25
|
return opts.botMsgId;
|
|
19
26
|
},
|
|
20
27
|
edit: async (msgId, text) => {
|
|
21
|
-
opts.
|
|
28
|
+
broadcast(opts.chatId, 'delta', { msgId, text });
|
|
22
29
|
},
|
|
23
30
|
},
|
|
24
31
|
throttleMs: WEBCHAT_THROTTLE_MS,
|
|
25
32
|
maxLength: WEBCHAT_MAX,
|
|
26
33
|
render: plainRender(WEBCHAT_MAX),
|
|
27
34
|
});
|
|
35
|
+
let answerText = null;
|
|
36
|
+
let answerReasoning = null;
|
|
37
|
+
let hasAnswer = false;
|
|
38
|
+
let persisted = false;
|
|
39
|
+
let afterwordWork = Promise.resolve();
|
|
28
40
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
41
|
+
emit(e) {
|
|
42
|
+
if (e.kind === 'reasoning') {
|
|
43
|
+
if (opts.display !== 'off')
|
|
44
|
+
broadcast(opts.chatId, 'reasoning', { msgId: opts.botMsgId, text: e.text });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (e.kind === 'suggestions') {
|
|
48
|
+
lastSuggestions = e.items;
|
|
49
|
+
if (recordedId && e.items.length) {
|
|
50
|
+
void recordSuggestionsFn({
|
|
51
|
+
chatId: opts.chatId,
|
|
52
|
+
botMsgId: opts.botMsgId,
|
|
53
|
+
parentMsgId: envelope.turnId,
|
|
54
|
+
suggestions: e.items,
|
|
55
|
+
now: Math.floor(Date.now() / 1000),
|
|
56
|
+
}).catch((err) => {
|
|
57
|
+
log.warn(`recordSuggestions failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (e.kind === 'title') {
|
|
63
|
+
afterwordWork = afterwordWork.then(() => setSelectionFn(opts.chatId, { title: e.text }).catch((err) => {
|
|
64
|
+
log.warn(`setSelection(title) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
|
+
}));
|
|
36
66
|
return;
|
|
37
|
-
|
|
67
|
+
}
|
|
68
|
+
if (e.kind === 'answer') {
|
|
69
|
+
answerText = e.text;
|
|
70
|
+
answerReasoning = e.reasoning;
|
|
71
|
+
hasAnswer = true;
|
|
72
|
+
}
|
|
73
|
+
inner.emit(e);
|
|
74
|
+
},
|
|
75
|
+
async done() {
|
|
76
|
+
await inner.done();
|
|
77
|
+
if (hasAnswer && !persisted) {
|
|
78
|
+
persisted = true;
|
|
79
|
+
const now = Math.floor(Date.now() / 1000);
|
|
80
|
+
const wrote = await record({
|
|
81
|
+
chatId: opts.chatId,
|
|
82
|
+
parentMsgId: envelope.turnId,
|
|
83
|
+
botMsgId: opts.botMsgId,
|
|
84
|
+
text: answerText ?? '',
|
|
85
|
+
now,
|
|
86
|
+
});
|
|
87
|
+
recordedId = wrote ? opts.botMsgId : null;
|
|
88
|
+
if (wrote && opts.display !== 'off' && answerReasoning) {
|
|
89
|
+
await recordReasoningFn({
|
|
90
|
+
chatId: opts.chatId,
|
|
91
|
+
botMsgId: opts.botMsgId,
|
|
92
|
+
parentMsgId: envelope.turnId,
|
|
93
|
+
text: answerReasoning,
|
|
94
|
+
now,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await afterwordWork;
|
|
38
99
|
},
|
|
39
100
|
};
|
|
40
101
|
},
|
|
41
|
-
async
|
|
42
|
-
|
|
43
|
-
const wrote = await record({
|
|
44
|
-
chatId: opts.chatId,
|
|
45
|
-
parentMsgId: m.parentMsgId,
|
|
46
|
-
botMsgId: m.botMsgId,
|
|
47
|
-
text: m.text,
|
|
48
|
-
now,
|
|
49
|
-
});
|
|
50
|
-
recordedId = wrote ? m.botMsgId : null;
|
|
51
|
-
if (wrote && opts.display !== 'off' && m.reasoning && m.botMsgId) {
|
|
52
|
-
await recordReasoningFn({
|
|
53
|
-
chatId: opts.chatId,
|
|
54
|
-
botMsgId: m.botMsgId,
|
|
55
|
-
parentMsgId: m.parentMsgId,
|
|
56
|
-
text: m.reasoning,
|
|
57
|
-
now,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (wrote && lastSuggestions?.length && m.botMsgId) {
|
|
61
|
-
await recordSuggestionsFn({
|
|
62
|
-
chatId: opts.chatId,
|
|
63
|
-
botMsgId: m.botMsgId,
|
|
64
|
-
parentMsgId: m.parentMsgId,
|
|
65
|
-
suggestions: lastSuggestions,
|
|
66
|
-
now,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
102
|
+
async recordContext(m) {
|
|
103
|
+
await recordContextFn({ ...m, chatId: opts.chatId });
|
|
69
104
|
},
|
|
70
105
|
};
|
|
71
106
|
return {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
|
|
|
2
2
|
export type { ThreadSummary, HistoryMsg } from './actions.ts';
|
|
3
3
|
export { WEB_FORMAT } from './format.ts';
|
|
4
4
|
export { makeStreamingConnector } from './connector.ts';
|
|
5
|
-
export { chatIdFor,
|
|
5
|
+
export { chatIdFor, recordUser, recordAssistant, buildChain, getSelection, setSelection } from './chain-store.ts';
|
|
6
6
|
export { sendAction, pageContext } from './send.ts';
|
|
7
7
|
export declare const serverHooks: PluginHooks;
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pruneThreadMessages } from '@coffer-org/server/thread-store';
|
|
2
2
|
import { pruneThreadState } from '@coffer-org/server/thread-state';
|
|
3
|
-
import { threadsAction, historyAction, agentsAction, selectAgentAction } from "./actions.js";
|
|
3
|
+
import { threadsAction, historyAction, agentsAction, selectAgentAction, setVisibilityAction, channelAction, stopAction, } from "./actions.js";
|
|
4
4
|
import { sendAction } from "./send.js";
|
|
5
5
|
import { CONNECTOR } from "./chain-store.js";
|
|
6
6
|
import { registerConnector } from '@coffer-org/server/orchestrator';
|
|
@@ -8,7 +8,7 @@ const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400
|
|
|
8
8
|
let unregisterConnector;
|
|
9
9
|
export { WEB_FORMAT } from "./format.js";
|
|
10
10
|
export { makeStreamingConnector } from "./connector.js";
|
|
11
|
-
export { chatIdFor,
|
|
11
|
+
export { chatIdFor, recordUser, recordAssistant, buildChain, getSelection, setSelection } from "./chain-store.js";
|
|
12
12
|
export { sendAction, pageContext } from "./send.js";
|
|
13
13
|
export const serverHooks = {
|
|
14
14
|
init: () => {
|
|
@@ -23,9 +23,12 @@ export const serverHooks = {
|
|
|
23
23
|
history: historyAction,
|
|
24
24
|
agents: agentsAction,
|
|
25
25
|
selectAgent: selectAgentAction,
|
|
26
|
+
setVisibility: (body, caller) => setVisibilityAction(body, caller),
|
|
27
|
+
send: (body, caller) => sendAction(body, caller),
|
|
28
|
+
stop: (body, caller) => stopAction(body, caller),
|
|
26
29
|
},
|
|
27
30
|
streamActions: {
|
|
28
|
-
|
|
31
|
+
channel: (body, ctx) => channelAction(body, ctx),
|
|
29
32
|
},
|
|
30
33
|
backgroundTasks: [
|
|
31
34
|
{
|
package/dist/runtime/send.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { handleIncoming } from '@coffer-org/server/orchestrator';
|
|
2
|
-
import type
|
|
2
|
+
import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
|
|
3
|
+
import type { ConnectorCapabilities, ContextFact } from '@coffer-org/server/orchestrator';
|
|
4
|
+
export declare const CAPABILITIES: ConnectorCapabilities;
|
|
3
5
|
export interface SendDeps {
|
|
4
6
|
handleIncoming?: typeof handleIncoming;
|
|
5
7
|
}
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export declare function pageContext(ctx: unknown): ContextFact[];
|
|
9
|
+
export interface SendResult {
|
|
10
|
+
msgId: string | null;
|
|
11
|
+
botMsgId: string | null;
|
|
10
12
|
}
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function sendAction(body: Record<string, unknown>, ctx: StreamCtx, deps?: SendDeps): Promise<void>;
|
|
13
|
+
export declare function sendAction(body: Record<string, unknown>, caller: ActionCaller, deps?: SendDeps): Promise<SendResult>;
|