ai_co_discuss 0.3.13 → 0.3.14
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/backend/free_room.py +28 -2
- package/package.json +1 -1
- package/runner/free_runner.mjs +9 -7
- package/runner/scheduler.mjs +57 -17
- package/runner/session_driver.mjs +3 -2
- package/web/app.js +6 -1
- package/web/modern.css +1 -1
- package/web/modern.js +1538 -1429
package/backend/free_room.py
CHANGED
|
@@ -1727,6 +1727,8 @@ def route_wide(connection, room_id, message_id, payload):
|
|
|
1727
1727
|
return route_group(connection, room_id, group, payload)
|
|
1728
1728
|
if action in ("sequential", "host"):
|
|
1729
1729
|
raise ValueError("只有请全体成员回应的消息可以依次回答或交回主持人")
|
|
1730
|
+
if action not in ("all", "none", "some"):
|
|
1731
|
+
raise ValueError("未点名消息只能选 action=all、some 或 none")
|
|
1730
1732
|
route = host_route_request(connection, message_id)
|
|
1731
1733
|
if not route or route["room_id"] != room_id:
|
|
1732
1734
|
raise LookupError("只能处理交给主持人的未点名消息")
|
|
@@ -1735,6 +1737,8 @@ def route_wide(connection, room_id, message_id, payload):
|
|
|
1735
1737
|
if action == "none":
|
|
1736
1738
|
record_cancel_reasons(connection, "id=?", (route["id"],), "no_reply")
|
|
1737
1739
|
connection.execute("UPDATE member_requests SET status='cancelled' WHERE id=?", (route["id"],))
|
|
1740
|
+
elif action == "some":
|
|
1741
|
+
route_some(connection, room_id, route, payload.get("members"))
|
|
1738
1742
|
else:
|
|
1739
1743
|
# The host keeps its row (one request per message and member) and now answers its own share, without dispatching.
|
|
1740
1744
|
connection.execute("UPDATE member_requests SET purpose='all' WHERE id=?", (route["id"],))
|
|
@@ -1745,6 +1749,28 @@ def route_wide(connection, room_id, message_id, payload):
|
|
|
1745
1749
|
return {"action": action, "requests": [dict(row) for row in requests]}
|
|
1746
1750
|
|
|
1747
1751
|
|
|
1752
|
+
def route_some(connection, room_id, route, names):
|
|
1753
|
+
"""Smart scheduling answers an unnamed message with the few members it names, each once, as if the user named them."""
|
|
1754
|
+
if not isinstance(names, list) or not all(isinstance(name, str) for name in names) or len(set(names)) < 2:
|
|
1755
|
+
raise ValueError("action=some 须带 members:至少两位不同的成员名")
|
|
1756
|
+
found = {row["name"]: row["id"] for row in connection.execute(
|
|
1757
|
+
f"SELECT id,name FROM room_members WHERE room_id=? AND status='active' AND {REGISTERED_MEMBER}", (room_id,))}
|
|
1758
|
+
missing = [name for name in dict.fromkeys(names) if name not in found]
|
|
1759
|
+
if missing:
|
|
1760
|
+
raise FileExistsError(f"这些成员不在场或未注册:{'、'.join(missing)}")
|
|
1761
|
+
ids = [found[name] for name in dict.fromkeys(names)]
|
|
1762
|
+
if route["member_id"] in ids:
|
|
1763
|
+
# The host is one of them: it keeps its row and answers its own share, without dispatching.
|
|
1764
|
+
connection.execute("UPDATE member_requests SET purpose='all' WHERE id=?", (route["id"],))
|
|
1765
|
+
else:
|
|
1766
|
+
record_cancel_reasons(connection, "id=?", (route["id"],), "rerouted")
|
|
1767
|
+
connection.execute("UPDATE member_requests SET status='cancelled' WHERE id=?", (route["id"],))
|
|
1768
|
+
for member_id in ids:
|
|
1769
|
+
if member_id != route["member_id"]:
|
|
1770
|
+
connection.execute("""INSERT OR IGNORE INTO member_requests (room_id,member_id,message_id,chain_id,purpose,created_by,created_at)
|
|
1771
|
+
VALUES (?,?,?,?,'all','user',?)""", (room_id, member_id, route["message_id"], route["message_id"], int(time.time())))
|
|
1772
|
+
|
|
1773
|
+
|
|
1748
1774
|
def adopt_recommendation(connection, room_id, token, message_id, payload):
|
|
1749
1775
|
"""The user adopts a recommendation before it expires: interrupt the host now, or ask the member after the host.
|
|
1750
1776
|
|
|
@@ -1752,10 +1778,10 @@ def adopt_recommendation(connection, room_id, token, message_id, payload):
|
|
|
1752
1778
|
"""
|
|
1753
1779
|
require_owner(connection, room_id, token)
|
|
1754
1780
|
begin_write(connection)
|
|
1755
|
-
if payload.get("action") in ("all", "none", "sequential", "host"):
|
|
1781
|
+
if payload.get("action") in ("all", "none", "sequential", "host", "some"):
|
|
1756
1782
|
return route_wide(connection, room_id, message_id, payload)
|
|
1757
1783
|
if payload.get("action") != "adopt" or payload.get("mode") not in ("interrupt", "after"):
|
|
1758
|
-
raise ValueError("采纳须为 action=adopt,mode 为 interrupt 或 after
|
|
1784
|
+
raise ValueError("采纳须为 action=adopt,mode 为 interrupt 或 after;全体、依次、指定几位或无需回应为 action=all、sequential、some 或 none")
|
|
1759
1785
|
row = connection.execute("SELECT * FROM member_recommendations WHERE room_id=? AND message_id=?", (room_id, message_id)).fetchone()
|
|
1760
1786
|
if not row:
|
|
1761
1787
|
raise LookupError("这条消息没有智能调度推荐")
|
package/package.json
CHANGED
package/runner/free_runner.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { SegmentLog } from './execution_progress.mjs';
|
|
|
9
9
|
import { free_task_text, TASK_SUBCOMMANDS, with_expert_prompt } from './expert_prompts.mjs';
|
|
10
10
|
import { atomic_json } from './files.mjs';
|
|
11
11
|
import { SessionDriver } from './session_driver.mjs';
|
|
12
|
-
import { DEFAULT_SCHEDULER_MODE, REVIEW_CONFIDENCE, REVIEW_PROMPT_VERSION, ROUTE_CONFIDENCE, ROUTE_CONTEXT_MESSAGES, ROUTE_PROMPT_VERSION, ROUTE_WIDE_CONFIDENCE, decide_group_order, decide_review_completion, route_free_message, route_prompt_version } from './scheduler.mjs';
|
|
12
|
+
import { DEFAULT_SCHEDULER_MODE, REVIEW_CONFIDENCE, REVIEW_PROMPT_VERSION, ROUTE_CONFIDENCE, ROUTE_CONTEXT_MESSAGES, ROUTE_PROMPT_VERSION, ROUTE_WIDE_CONFIDENCE, decide_group_order, decide_review_completion, route_free_message, route_prompt_version, scheduler_thresholds, some_members } from './scheduler.mjs';
|
|
13
13
|
|
|
14
14
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
15
15
|
const RENEW_MS = 30_000;
|
|
@@ -142,7 +142,8 @@ function member_work(requests, name, message_id) {
|
|
|
142
142
|
/** Why a message stayed with the host: the scheduler chose the host, or leaned elsewhere without enough certainty. */
|
|
143
143
|
function route_host_label(decision, host, members) {
|
|
144
144
|
if (decision.choice === '__host__' || decision.confidence === null) return `交给主持人 ${host}`;
|
|
145
|
-
const
|
|
145
|
+
const group = some_members(decision.choice);
|
|
146
|
+
const lean = decision.choice === '__all__' ? '全体各自回应' : decision.choice === '__none__' ? '无需回应' : group ? `请 ${group.join('、')} 各自回应`
|
|
146
147
|
: members.some(member => member.name === decision.choice) ? `请 ${decision.choice} 回答` : '';
|
|
147
148
|
return lean ? `倾向${lean},把握不足,交给主持人 ${host}` : `交给主持人 ${host}`;
|
|
148
149
|
}
|
|
@@ -452,7 +453,7 @@ export class FreeRoomRunner {
|
|
|
452
453
|
if (decision.detail) {
|
|
453
454
|
if (result.status === 'failed') label += '(未生效,网站写入失败)';
|
|
454
455
|
// The @全体成员 question has one wording in every mode.
|
|
455
|
-
record = await this.record_decision(request.message_id, decision, label, result.applied && decision.action !== 'host', { prompt: ROUTE_PROMPT_VERSION });
|
|
456
|
+
record = await this.record_decision(request.message_id, decision, label, result.applied && decision.action !== 'host', { prompt: ROUTE_PROMPT_VERSION, threshold: ROUTE_CONFIDENCE, wide_threshold: ROUTE_WIDE_CONFIDENCE });
|
|
456
457
|
}
|
|
457
458
|
this.save_suggestion(request, { action: decision.action, actual_action,
|
|
458
459
|
applied: result.applied && decision.action !== 'host', model_judgment: Boolean(decision.detail),
|
|
@@ -521,11 +522,11 @@ export class FreeRoomRunner {
|
|
|
521
522
|
this.log(`智能调度 #${message.id}:${decision.action}${decision.member ? ` ${decision.member}` : ''}(选择 ${decision.choice ?? '-'},置信度 ${decision.confidence ?? '-'})`);
|
|
522
523
|
if (decision.action === 'host') return this.record_route(request, decision, route_host_label(decision, host.name, members), false);
|
|
523
524
|
if (decision.action !== 'member') {
|
|
524
|
-
// Everyone or no one replaces the host's request in one step; a host that already started keeps the message.
|
|
525
|
-
const applied = await api(this.config, `/recommendations/${message.id}`, { action: decision.action })
|
|
525
|
+
// Everyone, the named few or no one replaces the host's request in one step; a host that already started keeps the message.
|
|
526
|
+
const applied = await api(this.config, `/recommendations/${message.id}`, { action: decision.action, ...(decision.members ? { members: decision.members } : {}) })
|
|
526
527
|
.then(() => true, error => { this.log(`智能调度 #${message.id} 未能改派:${error.message}`); return false; });
|
|
527
528
|
const names = described.map(member => member.name).join('、');
|
|
528
|
-
const label = decision.action === 'all' ? `请全体各自回应(${names})` : '无需回应,未唤醒任何成员';
|
|
529
|
+
const label = decision.action === 'all' ? `请全体各自回应(${names})` : decision.action === 'some' ? `请 ${decision.members.join('、')} 各自回应` : '无需回应,未唤醒任何成员';
|
|
529
530
|
return this.record_route(request, decision, applied ? label : `${label}(未生效,仍由主持人处理)`, applied);
|
|
530
531
|
}
|
|
531
532
|
try {
|
|
@@ -542,10 +543,11 @@ export class FreeRoomRunner {
|
|
|
542
543
|
}
|
|
543
544
|
/** Show each judgment under its message so the user can rate it; a failed record never affects delivery. */
|
|
544
545
|
async record_decision(message_id, decision, label, applied, context = {}) {
|
|
546
|
+
const limits = scheduler_thresholds(this.config.scheduler.mode || DEFAULT_SCHEDULER_MODE);
|
|
545
547
|
try {
|
|
546
548
|
return await api(this.config, '/scheduler-decisions', { kind: 'interjection', label, confidence: decision.confidence ?? null,
|
|
547
549
|
applied, provider: this.config.scheduler.provider, after_message_id: message_id, detail: decision.detail, input: decision.input,
|
|
548
|
-
context: { model: this.config.scheduler.model, threshold:
|
|
550
|
+
context: { model: this.config.scheduler.model, threshold: limits.member, wide_threshold: limits.wide,
|
|
549
551
|
mode: this.config.scheduler.mode || DEFAULT_SCHEDULER_MODE, prompt: route_prompt_version(this.config.scheduler.mode || DEFAULT_SCHEDULER_MODE), package: PACKAGE_VERSION, ...context } });
|
|
550
552
|
} catch (error) { this.log(`智能调度 #${message_id} 决策记录未保存:${error.message}`); }
|
|
551
553
|
}
|
package/runner/scheduler.mjs
CHANGED
|
@@ -11,8 +11,18 @@ const REQUEST_TIMEOUT_MS = 30_000;
|
|
|
11
11
|
export const ROUTE_CONFIDENCE = 0.7;
|
|
12
12
|
/** Waking everyone or no one costs more when wrong (N runs, or an ignored message), so it needs more certainty. */
|
|
13
13
|
export const ROUTE_WIDE_CONFIDENCE = 0.85;
|
|
14
|
+
/**
|
|
15
|
+
* The guided wording gives lower probabilities for the same correct pick, so it has its own thresholds, set where the
|
|
16
|
+
* regression set gains correct picks without any new wrong one (2026-10-31). @全体成员 and review keep their own.
|
|
17
|
+
*/
|
|
18
|
+
export const GUIDED_CONFIDENCE = 0.6;
|
|
19
|
+
export const GUIDED_WIDE_CONFIDENCE = 0.8;
|
|
20
|
+
/** The single-member and the everyone-or-no-one thresholds of one wording. */
|
|
21
|
+
export function scheduler_thresholds(mode) {
|
|
22
|
+
return mode === 'stable' ? { member: ROUTE_CONFIDENCE, wide: ROUTE_WIDE_CONFIDENCE } : { member: GUIDED_CONFIDENCE, wide: GUIDED_WIDE_CONFIDENCE };
|
|
23
|
+
}
|
|
14
24
|
/** Bump when the routing question or its option descriptions change, so ratings can be compared across wordings. */
|
|
15
|
-
export const ROUTE_PROMPT_VERSION =
|
|
25
|
+
export const ROUTE_PROMPT_VERSION = 13;
|
|
16
26
|
/**
|
|
17
27
|
* How the free-room routing question is worded. guided (experimental, the default while it is being refined) states the
|
|
18
28
|
* purpose and the evidence in prose; stable keeps the ordered checklist that passed the regression set.
|
|
@@ -20,7 +30,7 @@ export const ROUTE_PROMPT_VERSION = 12;
|
|
|
20
30
|
export const SCHEDULER_MODES = { guided: '引导(试验)', stable: '稳定' };
|
|
21
31
|
export const DEFAULT_SCHEDULER_MODE = 'guided';
|
|
22
32
|
/** The guided wording has its own version, so its ratings are never mixed with the stable checklist's. */
|
|
23
|
-
export const ROUTE_GUIDED_PROMPT_VERSION =
|
|
33
|
+
export const ROUTE_GUIDED_PROMPT_VERSION = 7;
|
|
24
34
|
/** The routing prompt version that a decision in this mode was made with. */
|
|
25
35
|
export function route_prompt_version(mode) {
|
|
26
36
|
return mode === 'stable' ? ROUTE_PROMPT_VERSION : ROUTE_GUIDED_PROMPT_VERSION;
|
|
@@ -28,7 +38,7 @@ export function route_prompt_version(mode) {
|
|
|
28
38
|
/** The same for the three discussion questions (interjection, next speaker, next round). */
|
|
29
39
|
export const DISCUSSION_PROMPT_VERSION = 5;
|
|
30
40
|
/** The guided wording of the discussion questions, versioned apart from the stable one. */
|
|
31
|
-
export const DISCUSSION_GUIDED_PROMPT_VERSION =
|
|
41
|
+
export const DISCUSSION_GUIDED_PROMPT_VERSION = 2;
|
|
32
42
|
/** The discussion prompt version that a decision in this mode was made with. */
|
|
33
43
|
export function discussion_prompt_version(mode) {
|
|
34
44
|
return mode === 'stable' ? DISCUSSION_PROMPT_VERSION : DISCUSSION_GUIDED_PROMPT_VERSION;
|
|
@@ -41,6 +51,11 @@ const HOST_CHOICE = '__host__';
|
|
|
41
51
|
const NOTE_CHOICE = '__note__';
|
|
42
52
|
const ALL_CHOICE = '__all__';
|
|
43
53
|
const NONE_CHOICE = '__none__';
|
|
54
|
+
/** Prefix of the "only these few" options; the value lists the names after it, joined by SOME_JOINER. */
|
|
55
|
+
export const SOME_PREFIX = '__some__:';
|
|
56
|
+
const SOME_JOINER = '、';
|
|
57
|
+
/** At most this many named members are combined, so the options stay few (4 names give 11 groups). */
|
|
58
|
+
const MAX_SOME_NAMES = 4;
|
|
44
59
|
const CONTEXT_MESSAGES = 10;
|
|
45
60
|
/** A free-room route sees a longer stretch, enough to tell who has not spoken for a while. */
|
|
46
61
|
export const ROUTE_CONTEXT_MESSAGES = 20;
|
|
@@ -205,21 +220,41 @@ const ROUTE_GUIDED_GUIDE = ['这是 AI CoDiscuss 的协作房间。用户是房
|
|
|
205
220
|
'唤醒一位成员要花一次执行,通常几十秒;交给了不对的人,就得打断后重来。'].join('\n');
|
|
206
221
|
const ROUTE_GUIDED_STEM = '用户发来上面这条消息,没有 @ 任何人。请你像熟悉这个房间的成员一样读懂它:用户想让谁来回应?'
|
|
207
222
|
+ '用户心里已经有明确的对象时,直接交给对方,省掉主持人转一手的时间;需要有人先理解、拆分或协调时交给主持人。'
|
|
208
|
-
+ '用户想听每个人各自说一句时,交给全体能让大家同时开始,比主持人逐个点名快得多;但大家看不到彼此的回答,需要互相接话、讨论的事仍要主持人安排。用户只是表示收到或感谢时,不必唤醒任何人。'
|
|
223
|
+
+ '用户想听每个人各自说一句时,交给全体能让大家同时开始,比主持人逐个点名快得多;但大家看不到彼此的回答,需要互相接话、讨论的事仍要主持人安排。用户只是表示收到或感谢时,不必唤醒任何人。'
|
|
224
|
+
+ '两种容易看错的情况:向在场的各位问好,是在等每个人回一声,和只对房间说一句“hi”不同;用户问“谁是……”“谁能……”时是在找符合条件的人,只需要符合的人出来回应(哪怕说“自己说一下”),成员信息里(含主持人)只有一位符合就交给他。';
|
|
209
225
|
|
|
210
226
|
/** The routing question in one mode: option texts, the stem, and what the state adds about the members. */
|
|
211
|
-
|
|
227
|
+
/** Every group of two or more among the members the text names, so "these few each answer" is one choice. */
|
|
228
|
+
function named_groups(text, host, members) {
|
|
229
|
+
const lower = String(text || '').toLowerCase();
|
|
230
|
+
const named = [host, ...members].map(member => member.name).filter(name => lower.includes(name.toLowerCase()));
|
|
231
|
+
if (named.length < 2 || named.length > MAX_SOME_NAMES) return [];
|
|
232
|
+
const groups = [];
|
|
233
|
+
for (let mask = 1; mask < 1 << named.length; mask += 1) {
|
|
234
|
+
const group = named.filter((_, index) => mask & (1 << index));
|
|
235
|
+
if (group.length >= 2) groups.push(group);
|
|
236
|
+
}
|
|
237
|
+
return groups;
|
|
238
|
+
}
|
|
239
|
+
/** The members of a "these few" choice, or null for any other value. */
|
|
240
|
+
export function some_members(choice) {
|
|
241
|
+
return typeof choice === 'string' && choice.startsWith(SOME_PREFIX) ? choice.slice(SOME_PREFIX.length).split(SOME_JOINER) : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function route_question(mode, host, members, text) {
|
|
212
245
|
if (mode === 'stable') return { extra: { roster: [`${host.name}(主持人)`, ...members.map(member => `${member.name}(专家)`)].join('、') },
|
|
213
246
|
instructions: ROUTE_STABLE_STEM, choices: [...members.map(member => ({ value: member.name, description: member.description })),
|
|
214
247
|
{ value: HOST_CHOICE, description: `主持人 ${host.description}:没有指向任何人且用户没有确定负责人(如单独一句“hi”“你好”这类问候、闲聊,由主持人代表房间回应),或诉求模糊、复杂,需要主持人判断怎么安排` },
|
|
215
248
|
{ value: ALL_CHOICE, description: '全体成员(含主持人)各自回应一次,彼此不需要看到别人的回答;只在要求简单明确、每人独立回应一次就算完成时选,如“大家打个招呼”“各自报一下在用的模型”' },
|
|
216
|
-
{ value: NONE_CHOICE, description: '
|
|
249
|
+
{ value: NONE_CHOICE, description: '无需回应:只是确认、致谢或旁注,不需要任何人回答;选它则用户这句话不会得到回应' }] };
|
|
217
250
|
const people = ['成员(名字不区分大小写):', guided_member_line('主持人', host), ...members.map(member => guided_member_line('专家', member))].join('\n');
|
|
218
251
|
return { extra: { guide: ROUTE_GUIDED_GUIDE, people }, instructions: ROUTE_GUIDED_STEM,
|
|
219
252
|
choices: [...members.map(member => ({ value: member.name, description: `交给 ${member.name} 回应` })),
|
|
220
253
|
{ value: HOST_CHOICE, description: `交给主持人 ${host.name}:由他回应,或由他安排别人` },
|
|
254
|
+
...named_groups(text, host, members).map(group => ({ value: `${SOME_PREFIX}${group.join(SOME_JOINER)}`,
|
|
255
|
+
description: `只请 ${group.join('、')} 各自回应一次:同时开始,每人都收到这句原话,彼此看不到对方的回答,其他成员不被唤醒` })),
|
|
221
256
|
{ value: ALL_CHOICE, description: '全体成员(含主持人)同时收到这句原话,各自回应一次,彼此看不到对方的回答,也不会被分到不同的任务' },
|
|
222
|
-
{ value: NONE_CHOICE, description: '
|
|
257
|
+
{ value: NONE_CHOICE, description: '不唤醒任何人,用户这句话不会得到回应' }] };
|
|
223
258
|
}
|
|
224
259
|
|
|
225
260
|
/**
|
|
@@ -228,17 +263,21 @@ function route_question(mode, host, members) {
|
|
|
228
263
|
* all, none, or host for anything unsettled or not confident enough.
|
|
229
264
|
*/
|
|
230
265
|
export async function route_free_message(home, scheduler, { title, goal, context, message, host, members }, fetch_impl) {
|
|
231
|
-
const { choices, instructions, extra } = route_question(scheduler.mode || DEFAULT_SCHEDULER_MODE, host, members);
|
|
266
|
+
const { choices, instructions, extra } = route_question(scheduler.mode || DEFAULT_SCHEDULER_MODE, host, members, message.text);
|
|
232
267
|
const questions = [{ type: 'choice', name: 'handler', choices, instructions }];
|
|
233
268
|
const state = scheduler_state(title, context, message, goal, '', extra, ROUTE_CONTEXT_MESSAGES);
|
|
234
269
|
const answers = await decide(home, scheduler, state, questions, fetch_impl);
|
|
235
270
|
const judged = judged_of(state, questions, answers);
|
|
236
271
|
const choice = answers.handler?.choice ?? null;
|
|
237
272
|
const confidence = certainty(answers.handler);
|
|
238
|
-
|
|
239
|
-
const
|
|
273
|
+
// Waking several members, everyone or no one costs more when wrong than moving the message to one member.
|
|
274
|
+
const group = some_members(choice);
|
|
275
|
+
const wide = choice === ALL_CHOICE || choice === NONE_CHOICE || Boolean(group);
|
|
276
|
+
const limits = scheduler_thresholds(scheduler.mode || DEFAULT_SCHEDULER_MODE);
|
|
277
|
+
const sure = confidence !== null && confidence >= (wide ? limits.wide : limits.member);
|
|
240
278
|
if (sure && choice === ALL_CHOICE) return { action: 'all', choice, confidence, ...judged };
|
|
241
279
|
if (sure && choice === NONE_CHOICE) return { action: 'none', choice, confidence, ...judged };
|
|
280
|
+
if (sure && group) return { action: 'some', members: group, choice, confidence, ...judged };
|
|
242
281
|
if (sure && members.some(item => item.name === choice)) return { action: 'member', member: choice, choice, confidence, ...judged };
|
|
243
282
|
return { action: 'host', choice, confidence, ...judged };
|
|
244
283
|
}
|
|
@@ -272,14 +311,14 @@ export async function decide_interjection(home, scheduler, { title, goal, user_r
|
|
|
272
311
|
const choices = guided
|
|
273
312
|
? [...experts.map(name => ({ value: name, description: `现在单独请 ${name} 回应这条留言:不占讨论轮次,多花一次执行` })),
|
|
274
313
|
{ value: HOST_CHOICE, description: `交给主持人 ${host} 处理` },
|
|
275
|
-
{ value: NOTE_CHOICE, description: '
|
|
314
|
+
{ value: NOTE_CHOICE, description: '按原安排继续:讨论照常推进、不会暂停,留言留在房间里,后面发言的专家都会看到并可在发言中回应' }]
|
|
276
315
|
: [...experts.map(name => ({ value: name, description: `请 ${name} 回应这条留言:留言是向该专家(或其负责的内容)提问、质疑或补充要求` })),
|
|
277
316
|
{ value: HOST_CHOICE, description: `由主持人 ${host} 处理:留言要改变讨论目标、规则、分工、安排或结束讨论,同时涉及多位专家,或针对的是不在选项里的人` },
|
|
278
317
|
{ value: NOTE_CHOICE, description: '仅补充信息:留言补充背景、约束或偏好,或只是旁注、鼓励,没有要求某人立即回应;后续发言的专家都能看到它,按当前安排继续' }];
|
|
279
318
|
const instructions = guided
|
|
280
319
|
? '讨论进行中,用户发来上面这条留言。请你像熟悉这场讨论的人一样读懂它,选出最合适的处理方式。'
|
|
281
320
|
+ '用户向某位专家要回应时,现在单独请他回答能马上得到答复,但要多花一次执行;如果他就在接下来要发言的人里,留到他发言时一并回应更省,也不打乱讨论。'
|
|
282
|
-
+ '
|
|
321
|
+
+ '补充的约束、偏好和鼓励,后面发言的人都看得到。要暂停或结束讨论、改目标、规则、分工或安排,或需要几位专家协调的,由主持人处理。'
|
|
283
322
|
: '讨论进行中用户发来这条留言。对照当前焦点、主持简报和安排,判断它最适合怎样处理。';
|
|
284
323
|
const questions = [{ type: 'choice', name: 'handling', instructions, choices }];
|
|
285
324
|
const state = scheduler_state(title, context, message, goal, user_request, { ...extra, progress: round_progress(spoken, experts.filter(name => !spoken.includes(name))),
|
|
@@ -288,7 +327,7 @@ export async function decide_interjection(home, scheduler, { title, goal, user_r
|
|
|
288
327
|
const judged = judged_of(state, questions, answers);
|
|
289
328
|
const choice = answers.handling?.choice;
|
|
290
329
|
const confidence = certainty(answers.handling);
|
|
291
|
-
const sure = confidence !== null && confidence >=
|
|
330
|
+
const sure = confidence !== null && confidence >= scheduler_thresholds(scheduler.mode || DEFAULT_SCHEDULER_MODE).member;
|
|
292
331
|
if (sure && choice === NOTE_CHOICE) return { action: 'note', confidence, ...judged, label: '仅补充信息,按原计划继续' };
|
|
293
332
|
if (sure && experts.includes(choice)) return { action: 'ask', expert: choice, confidence, ...judged, label: `先请 ${choice} 回应这条留言` };
|
|
294
333
|
const lean = choice === NOTE_CHOICE ? '按原计划继续' : experts.includes(choice) ? `请 ${choice} 回应` : choice === HOST_CHOICE ? '由主持人处理' : '';
|
|
@@ -338,7 +377,7 @@ export async function decide_next_speaker(home, scheduler, { title, goal, user_r
|
|
|
338
377
|
const judged = judged_of(state, questions, answers);
|
|
339
378
|
const choice = answers.speaker?.choice;
|
|
340
379
|
const confidence = certainty(answers.speaker);
|
|
341
|
-
const sure = confidence !== null && confidence >=
|
|
380
|
+
const sure = confidence !== null && confidence >= scheduler_thresholds(scheduler.mode || DEFAULT_SCHEDULER_MODE).member;
|
|
342
381
|
if (sure && candidates.includes(choice)) return { action: 'next', expert: choice, confidence, ...judged, label: `本轮继续,请 ${choice} 接着发言` };
|
|
343
382
|
return { action: 'host', confidence, ...judged, label: choice === HOST_CHOICE && sure ? '没有明确该谁发言,交给主持人安排' : unsure_label(candidates.includes(choice) ? `请 ${choice} 发言` : choice === HOST_CHOICE ? '交给主持人' : '') };
|
|
344
383
|
}
|
|
@@ -373,13 +412,14 @@ export async function decide_round(home, scheduler, { title, goal, user_request,
|
|
|
373
412
|
const choice = answers.next?.choice;
|
|
374
413
|
const confidence = certainty(answers.next);
|
|
375
414
|
// A wrong "continue" costs a whole round of runs, so it needs the same certainty as waking everyone.
|
|
376
|
-
|
|
377
|
-
|
|
415
|
+
const limits = scheduler_thresholds(scheduler.mode || DEFAULT_SCHEDULER_MODE);
|
|
416
|
+
if (choice !== 'continue' || confidence === null || confidence < limits.wide) {
|
|
417
|
+
const sure = confidence !== null && confidence >= (choice === 'continue' ? limits.wide : limits.member);
|
|
378
418
|
return { action: 'host', confidence, ...judged, label: choice === HOST_CHOICE && sure ? '建议收束或调整方向,交给主持人' : unsure_label(choice === 'continue' ? '继续下一轮' : choice === HOST_CHOICE ? '收束或调整方向' : '') };
|
|
379
419
|
}
|
|
380
420
|
const first = answers.first?.choice;
|
|
381
421
|
const first_confidence = certainty(answers.first);
|
|
382
|
-
if (!candidates.includes(first) || first_confidence === null || first_confidence <
|
|
422
|
+
if (!candidates.includes(first) || first_confidence === null || first_confidence < limits.member) {
|
|
383
423
|
return { action: 'host', confidence: first_confidence, ...judged, label: '下一轮首位没有明确依据或把握不足,交给主持人安排' };
|
|
384
424
|
}
|
|
385
425
|
return { action: 'continue', first, confidence, ...judged, label: `继续下一轮,先请 ${first} 发言` };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Smart scheduling and limit checks for a discussion hosted inside a free room. */
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { api, peer_round_question } from './discussion_runner.mjs';
|
|
4
|
-
import { DEFAULT_SCHEDULER_MODE,
|
|
4
|
+
import { DEFAULT_SCHEDULER_MODE, ROUTE_CONTEXT_MESSAGES, scheduler_thresholds, decide_interjection, decide_next_speaker, decide_round, discussion_prompt_version } from './scheduler.mjs';
|
|
5
5
|
|
|
6
6
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
7
7
|
/** How many of the latest plans describe "the host's recent arrangement" to the scheduler. */
|
|
@@ -200,7 +200,8 @@ export class SessionDriver {
|
|
|
200
200
|
return await api(this.config, '/scheduler-decisions', { kind: plan.kind, label: plan.label,
|
|
201
201
|
confidence: plan.confidence ?? null, applied, provider: this.config.scheduler.provider,
|
|
202
202
|
after_message_id, detail: plan.detail, input: plan.input,
|
|
203
|
-
context: { model: this.config.scheduler.model, threshold:
|
|
203
|
+
context: { model: this.config.scheduler.model, threshold: scheduler_thresholds(this.config.scheduler.mode || DEFAULT_SCHEDULER_MODE).member,
|
|
204
|
+
wide_threshold: scheduler_thresholds(this.config.scheduler.mode || DEFAULT_SCHEDULER_MODE).wide,
|
|
204
205
|
mode: this.config.scheduler.mode || DEFAULT_SCHEDULER_MODE, prompt: discussion_prompt_version(this.config.scheduler.mode || DEFAULT_SCHEDULER_MODE), package: PACKAGE_VERSION } });
|
|
205
206
|
} catch (error) { this.log(`智能调度决策记录未保存:${error.message}`); }
|
|
206
207
|
}
|
package/web/app.js
CHANGED
|
@@ -3264,6 +3264,11 @@ async function rate_decision({ decision_id, rating, should_be = "", note = "" })
|
|
|
3264
3264
|
if (state.room?.id === room_id) await refresh_room();
|
|
3265
3265
|
}
|
|
3266
3266
|
|
|
3267
|
+
/** Read what the decision model was given for one decision (state and questions), for the card's input view. */
|
|
3268
|
+
async function load_decision_input(decision_id) {
|
|
3269
|
+
return request_json(`/api/rooms/${state.room.id}/scheduler-decisions/${decision_id}`);
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3267
3272
|
/** A completed creation starts the next form with fresh mode defaults. */
|
|
3268
3273
|
function reset_creation_mode() {
|
|
3269
3274
|
state.creation_mode = "free";
|
|
@@ -4107,7 +4112,7 @@ async function execute_view_action(name, value) {
|
|
|
4107
4112
|
export_runtime, download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_runtime, check_limits, pause_discussion, restart_discussion, conclude_discussion, start_room_discussion, end_room_discussion, reopen_discussion,
|
|
4108
4113
|
save_launch_defaults, launch_current_room, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
|
|
4109
4114
|
load_room_configuration, save_room_configuration, interrupt_member, retry_delivery, retract_message, retract_latest, stop_room, rewind_and_send, fork_room, change_member, reorder_speakers, invite_member, set_proxy, copy_online_notice,
|
|
4110
|
-
prepare_brainstorm, return_conclusion, adopt_recommendation, rate_decision,
|
|
4115
|
+
prepare_brainstorm, return_conclusion, adopt_recommendation, rate_decision, load_decision_input,
|
|
4111
4116
|
busy: () => { throw new Error("上一个操作仍在处理中,请稍候;长时间无响应请刷新页面"); },
|
|
4112
4117
|
copy_reading: () => copy_text(`${location.origin}/r/${state.room.id}`), copy: copy_text };
|
|
4113
4118
|
try {
|