@goodandready/dsh-moa 0.2.69 → 0.2.71
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/lib/client.js +97 -0
- package/lib/history.js +21 -6
- package/lib/moa-adversarial-tdd.js +219 -0
- package/lib/moa-ast-merge.js +230 -0
- package/lib/moa-elo.js +189 -0
- package/lib/moa-router.js +27 -9
- package/lib/moa-schema.js +12 -0
- package/lib/moa-speculative.js +195 -0
- package/lib/moa-visual-consensus.js +187 -0
- package/lib/moa-workbench.js +18 -0
- package/lib/pipeline/candidates.js +42 -2
- package/lib/pipeline/promotion.js +4 -2
- package/lib/pipeline/runner.js +23 -6
- package/package.json +1 -1
package/lib/moa-elo.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empirical Elo Rating and Active Learning Leaderboard by Task Category (#249)
|
|
3
|
+
* Calculates pairwise Elo ratings (K-factor = 32, baseline = 1500) segmented by
|
|
4
|
+
* task category (frontend, backend, refactoring, testing, sql, general),
|
|
5
|
+
* and exposes category leaders to Smart Routing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const INITIAL_ELO = 1500
|
|
9
|
+
export const DEFAULT_K_FACTOR = 32
|
|
10
|
+
|
|
11
|
+
const CATEGORY_PATTERNS = [
|
|
12
|
+
{ category: 'sql', regex: /\b(sql|postgres|sqlite|mysql|query|database|schema|migration|prisma|index|table)\b/i },
|
|
13
|
+
{ category: 'frontend', regex: /\b(frontend|ui|css|html|react|vue|tailwind|component|button|layout|modal|svg|dom)\b/i },
|
|
14
|
+
{ category: 'testing', regex: /\b(test|unit\s*test|e2e|vitest|jest|playwright|assert|coverage|mock|tdd)\b/i },
|
|
15
|
+
{ category: 'refactoring', regex: /\b(refactor|clean|simplify|dedup|modular|rename|extract|decompose|yagni)\b/i },
|
|
16
|
+
{ category: 'backend', regex: /\b(backend|api|endpoint|server|route|auth|jwt|middleware|express|http|rest|webhook|stream)\b/i },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Classifies a prompt into one of the standard Elo leaderboard categories.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} prompt
|
|
23
|
+
* @returns {'frontend' | 'backend' | 'refactoring' | 'testing' | 'sql' | 'general'}
|
|
24
|
+
*/
|
|
25
|
+
export function classifyEloCategory(prompt = '') {
|
|
26
|
+
const text = String(prompt || '').trim()
|
|
27
|
+
if (!text) return 'general'
|
|
28
|
+
for (const rule of CATEGORY_PATTERNS) {
|
|
29
|
+
if (rule.regex.test(text)) return rule.category
|
|
30
|
+
}
|
|
31
|
+
return 'general'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Calculates expected win probability of player A against player B.
|
|
36
|
+
*/
|
|
37
|
+
export function expectedEloScore(ratingA, ratingB) {
|
|
38
|
+
return 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Updates pairwise Elo ratings when winnerKey beats all other participantKeys in a round.
|
|
43
|
+
*
|
|
44
|
+
* @param {Map<string, number>} ratingMap
|
|
45
|
+
* @param {string} winnerKey
|
|
46
|
+
* @param {string[]} participantKeys
|
|
47
|
+
* @param {number} [kFactor=32]
|
|
48
|
+
*/
|
|
49
|
+
export function applyRoundEloUpdate(ratingMap, winnerKey, participantKeys = [], kFactor = DEFAULT_K_FACTOR) {
|
|
50
|
+
if (!winnerKey || participantKeys.length < 2) return
|
|
51
|
+
const opponents = participantKeys.filter((k) => k && k !== winnerKey)
|
|
52
|
+
if (opponents.length === 0) return
|
|
53
|
+
|
|
54
|
+
// Normalize K-factor across the number of pairwise comparisons in the multi-candidate round
|
|
55
|
+
const effectiveK = kFactor / opponents.length
|
|
56
|
+
let winnerRating = ratingMap.get(winnerKey) ?? INITIAL_ELO
|
|
57
|
+
let winnerDelta = 0
|
|
58
|
+
|
|
59
|
+
for (const oppKey of opponents) {
|
|
60
|
+
const oppRating = ratingMap.get(oppKey) ?? INITIAL_ELO
|
|
61
|
+
const expWin = expectedEloScore(winnerRating, oppRating)
|
|
62
|
+
const expOpp = 1 - expWin
|
|
63
|
+
|
|
64
|
+
winnerDelta += effectiveK * (1 - expWin)
|
|
65
|
+
const nextOpp = oppRating + effectiveK * (0 - expOpp)
|
|
66
|
+
ratingMap.set(oppKey, nextOpp)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ratingMap.set(winnerKey, winnerRating + winnerDelta)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Computes global and per-category Elo ratings across a chronological list of MoA runs.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<object>} runs - Array of history run objects (oldest to newest)
|
|
76
|
+
* @param {number} [kFactor=32]
|
|
77
|
+
* @returns {{
|
|
78
|
+
* globalRatings: Map<string, number>,
|
|
79
|
+
* categoryRatings: Map<string, Map<string, number>>
|
|
80
|
+
* }}
|
|
81
|
+
*/
|
|
82
|
+
export function computeEloFromRuns(runs = [], kFactor = DEFAULT_K_FACTOR) {
|
|
83
|
+
const globalRatings = new Map()
|
|
84
|
+
const categoryRatings = new Map()
|
|
85
|
+
|
|
86
|
+
for (const run of runs) {
|
|
87
|
+
if (!run || !Array.isArray(run.candidates) || run.candidates.length === 0) continue
|
|
88
|
+
|
|
89
|
+
const category = run.category || classifyEloCategory(run.prompt || '')
|
|
90
|
+
if (!categoryRatings.has(category)) {
|
|
91
|
+
categoryRatings.set(category, new Map())
|
|
92
|
+
}
|
|
93
|
+
const catMap = categoryRatings.get(category)
|
|
94
|
+
|
|
95
|
+
const participantKeys = []
|
|
96
|
+
let winnerKey = null
|
|
97
|
+
|
|
98
|
+
const rawWinner = run.winnerModel ? String(run.winnerModel).trim() : null
|
|
99
|
+
const cleanWinner = rawWinner ? rawWinner.replace(/\s*\[fallback for .*\]$/i, '').trim() : null
|
|
100
|
+
const normWinner = cleanWinner && cleanWinner !== 'none' && cleanWinner !== 'unknown'
|
|
101
|
+
? cleanWinner.replace(/[\/:]/g, ':').toLowerCase()
|
|
102
|
+
: null
|
|
103
|
+
|
|
104
|
+
for (let idx = 0; idx < run.candidates.length; idx++) {
|
|
105
|
+
const cand = run.candidates[idx]
|
|
106
|
+
const key = cand.provider && cand.model ? `${cand.provider}:${cand.model}` : (cand.model || 'unknown')
|
|
107
|
+
participantKeys.push(key)
|
|
108
|
+
if (!globalRatings.has(key)) globalRatings.set(key, INITIAL_ELO)
|
|
109
|
+
if (!catMap.has(key)) catMap.set(key, INITIAL_ELO)
|
|
110
|
+
|
|
111
|
+
if (!winnerKey) {
|
|
112
|
+
if (normWinner) {
|
|
113
|
+
const normKey = key.replace(/[\/:]/g, ':').toLowerCase()
|
|
114
|
+
const normModel = String(cand.model || '').replace(/[\/:]/g, ':').toLowerCase()
|
|
115
|
+
if (normWinner === normKey || normWinner === normModel || normWinner.endsWith(`:${normModel}`)) {
|
|
116
|
+
winnerKey = key
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!winnerKey && typeof run.winnerIndex === 'number' && run.winnerIndex > 0) {
|
|
120
|
+
if (cand.index === run.winnerIndex || idx === run.winnerIndex - 1) {
|
|
121
|
+
winnerKey = key
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (winnerKey && participantKeys.length >= 2) {
|
|
128
|
+
applyRoundEloUpdate(globalRatings, winnerKey, participantKeys, kFactor)
|
|
129
|
+
applyRoundEloUpdate(catMap, winnerKey, participantKeys, kFactor)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { globalRatings, categoryRatings }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Selects the best preset for a prompt category based on historical Elo ratings of its reference models.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} prompt
|
|
140
|
+
* @param {Array<object>} presets
|
|
141
|
+
* @param {object} leaderboard - Result from getMoaLeaderboard()
|
|
142
|
+
* @returns {{ presetName: string, category: string, avgElo: number } | null}
|
|
143
|
+
*/
|
|
144
|
+
export function selectBestPresetByElo(prompt = '', presets = [], leaderboard = null) {
|
|
145
|
+
if (!leaderboard || !Array.isArray(leaderboard.models) || leaderboard.models.length === 0) return null
|
|
146
|
+
if (!Array.isArray(presets) || presets.length === 0) return null
|
|
147
|
+
|
|
148
|
+
const category = classifyEloCategory(prompt)
|
|
149
|
+
const modelMap = new Map()
|
|
150
|
+
for (const m of leaderboard.models) {
|
|
151
|
+
modelMap.set(m.modelKey, m)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let bestPreset = null
|
|
155
|
+
let bestElo = -Infinity
|
|
156
|
+
|
|
157
|
+
for (const preset of presets) {
|
|
158
|
+
if (preset.enabled === false) continue
|
|
159
|
+
const refs = Array.isArray(preset.reference_models) ? preset.reference_models : []
|
|
160
|
+
if (refs.length === 0) continue
|
|
161
|
+
|
|
162
|
+
let sumElo = 0
|
|
163
|
+
let matchedCount = 0
|
|
164
|
+
for (const ref of refs) {
|
|
165
|
+
const key = ref.provider && ref.model ? `${ref.provider}:${ref.model}` : ref.model
|
|
166
|
+
const st = modelMap.get(key)
|
|
167
|
+
if (st) {
|
|
168
|
+
const catElo = st.categoryElo?.[category] ?? st.elo ?? INITIAL_ELO
|
|
169
|
+
sumElo += catElo
|
|
170
|
+
matchedCount++
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (matchedCount > 0) {
|
|
175
|
+
const avgElo = sumElo / matchedCount
|
|
176
|
+
if (avgElo > bestElo && avgElo > INITIAL_ELO) {
|
|
177
|
+
bestElo = avgElo
|
|
178
|
+
bestPreset = preset.name
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!bestPreset) return null
|
|
184
|
+
return {
|
|
185
|
+
presetName: bestPreset,
|
|
186
|
+
category,
|
|
187
|
+
avgElo: Math.round(bestElo),
|
|
188
|
+
}
|
|
189
|
+
}
|
package/lib/moa-router.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectBestPresetByElo } from './moa-elo.js'
|
|
1
2
|
/**
|
|
2
3
|
* Smart Preset Router for Mixture of Agents (MoA).
|
|
3
4
|
* Supports heuristic keyword classification and optional LLM zero-shot routing.
|
|
@@ -32,15 +33,18 @@ export function classifyPromptIntent(prompt = '') {
|
|
|
32
33
|
/**
|
|
33
34
|
* Resolves the optimal preset for a given user prompt.
|
|
34
35
|
*/
|
|
35
|
-
export async function resolvePresetForPrompt({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
export async function resolvePresetForPrompt(arg1 = {}, arg2, arg3, arg4 = {}) {
|
|
37
|
+
const isPositional = typeof arg1 === "string"
|
|
38
|
+
const prompt = isPositional ? arg1 : (arg1.prompt || "")
|
|
39
|
+
const presets = isPositional ? (arg2 || []) : (arg1.presets || [])
|
|
40
|
+
const defaultPreset = isPositional ? (arg3 || "default") : (arg1.defaultPreset || "default")
|
|
41
|
+
const opts = isPositional ? (arg4 || {}) : arg1
|
|
42
|
+
const routingModel = opts.routingModel || null
|
|
43
|
+
const callLlm = opts.callLlm || null
|
|
44
|
+
const enabled = Boolean(opts.enabled ?? opts.smartRoutingEnabled ?? false)
|
|
45
|
+
const timeoutMs = opts.timeoutMs ?? 2000
|
|
46
|
+
const leaderboard = opts.leaderboard || null
|
|
47
|
+
const eloRankingEnabled = opts.eloRankingEnabled !== false
|
|
44
48
|
const availableNames = (presets || []).map((p) => p.name).filter(Boolean)
|
|
45
49
|
const safeDefault = availableNames.includes(defaultPreset) ? defaultPreset : (availableNames[0] || 'default')
|
|
46
50
|
|
|
@@ -87,6 +91,20 @@ Respond with ONLY the chosen preset name and nothing else.`
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
// 1.5. Active Learning Elo Leaderboard routing (#249)
|
|
95
|
+
if (leaderboard && eloRankingEnabled) {
|
|
96
|
+
const eloChoice = selectBestPresetByElo(prompt, presets, leaderboard)
|
|
97
|
+
if (eloChoice && availableNames.includes(eloChoice.presetName)) {
|
|
98
|
+
return {
|
|
99
|
+
presetName: eloChoice.presetName,
|
|
100
|
+
reason: "elo_active_learning",
|
|
101
|
+
category: eloChoice.category,
|
|
102
|
+
avgElo: eloChoice.avgElo,
|
|
103
|
+
isAutoRouted: true,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
// 2. Keyword heuristic classifier
|
|
91
109
|
const heuristic = classifyPromptIntent(prompt)
|
|
92
110
|
if (heuristic && availableNames.includes(heuristic)) {
|
package/lib/moa-schema.js
CHANGED
|
@@ -70,6 +70,12 @@ export const PresetSchema = z.object({
|
|
|
70
70
|
self_healing_enabled: z.boolean().default(false),
|
|
71
71
|
self_healing_max_retries: z.number().default(2),
|
|
72
72
|
self_healing_command: z.string().default("npm test"),
|
|
73
|
+
visual_moa_enabled: z.boolean().default(false),
|
|
74
|
+
elo_ranking_enabled: z.boolean().default(true),
|
|
75
|
+
ast_composite_merge_enabled: z.boolean().default(false),
|
|
76
|
+
speculative_moa_enabled: z.boolean().default(false),
|
|
77
|
+
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }),
|
|
78
|
+
adversarial_tdd_enabled: z.boolean().default(false),
|
|
73
79
|
})
|
|
74
80
|
|
|
75
81
|
export const Config = z.object({
|
|
@@ -86,6 +92,12 @@ export const Config = z.object({
|
|
|
86
92
|
self_healing_enabled: z.boolean().default(false).volatile(),
|
|
87
93
|
self_healing_max_retries: z.number().default(2).volatile(),
|
|
88
94
|
self_healing_command: z.string().default("npm test").volatile(),
|
|
95
|
+
visual_moa_enabled: z.boolean().default(false).volatile(),
|
|
96
|
+
elo_ranking_enabled: z.boolean().default(true).volatile(),
|
|
97
|
+
ast_composite_merge_enabled: z.boolean().default(false).volatile(),
|
|
98
|
+
speculative_moa_enabled: z.boolean().default(false).volatile(),
|
|
99
|
+
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }).volatile(),
|
|
100
|
+
adversarial_tdd_enabled: z.boolean().default(false).volatile(),
|
|
89
101
|
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
90
102
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
91
103
|
presets: z.array(PresetSchema).default([
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speculative MoA with Fast-Draft Model and Multi-Auditor Diffs (#247)
|
|
3
|
+
* Generates a rapid initial draft using speculative_draft_model, dispatches the draft
|
|
4
|
+
* to reference models in parallel as diff auditors (producing targeted SEARCH/REPLACE
|
|
5
|
+
* or unified diff hunks), applies valid hunks to the draft, and gracefully falls back
|
|
6
|
+
* to standard parallel MoA if the draft model fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
import { slotLabel } from './moa-prompts.js'
|
|
11
|
+
import { estimateTokenCost } from './pricing.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies SEARCH/REPLACE or unified diff patches onto a base draft text.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} draftText
|
|
17
|
+
* @param {string} patchText
|
|
18
|
+
* @returns {{ patchedText: string, appliedCount: number }}
|
|
19
|
+
*/
|
|
20
|
+
export function applyAuditorPatchHunks(draftText = '', patchText = '') {
|
|
21
|
+
let result = String(draftText || '')
|
|
22
|
+
let appliedCount = 0
|
|
23
|
+
|
|
24
|
+
// 1. Support SEARCH / REPLACE blocks:
|
|
25
|
+
// <<<<<<< SEARCH
|
|
26
|
+
// ...
|
|
27
|
+
// =======
|
|
28
|
+
// ...
|
|
29
|
+
// >>>>>>> REPLACE
|
|
30
|
+
const searchReplaceRegex = /<{5,9}\s*SEARCH\r?\n([\s\S]*?)\r?\n={5,9}\r?\n([\s\S]*?)\r?\n>{5,9}\s*REPLACE/g
|
|
31
|
+
let srMatch
|
|
32
|
+
while ((srMatch = searchReplaceRegex.exec(patchText)) !== null) {
|
|
33
|
+
const searchBlock = srMatch[1]
|
|
34
|
+
const replaceBlock = srMatch[2]
|
|
35
|
+
if (searchBlock && result.includes(searchBlock)) {
|
|
36
|
+
result = result.replace(searchBlock, replaceBlock)
|
|
37
|
+
appliedCount++
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Support unified diff hunks (- oldLine / + newLine)
|
|
42
|
+
if (appliedCount === 0 && /```diff[\r\n]+([\s\S]*?)```/i.test(patchText)) {
|
|
43
|
+
const diffMatch = /```diff[\r\n]+([\s\S]*?)```/i.exec(patchText)
|
|
44
|
+
const diffLines = diffMatch[1].split(/\r?\n/)
|
|
45
|
+
const removed = []
|
|
46
|
+
const added = []
|
|
47
|
+
for (const line of diffLines) {
|
|
48
|
+
if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')) continue
|
|
49
|
+
if (line.startsWith('-')) removed.push(line.slice(1))
|
|
50
|
+
else if (line.startsWith('+')) added.push(line.slice(1))
|
|
51
|
+
}
|
|
52
|
+
if (removed.length > 0) {
|
|
53
|
+
const targetChunk = removed.join('\n')
|
|
54
|
+
const replacementChunk = added.join('\n')
|
|
55
|
+
if (result.includes(targetChunk)) {
|
|
56
|
+
result = result.replace(targetChunk, replacementChunk)
|
|
57
|
+
appliedCount++
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { patchedText: result, appliedCount }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes Speculative MoA:
|
|
67
|
+
* 1. Calls fast draft model (`speculative_draft_model` or first reference slot).
|
|
68
|
+
* 2. Dispatches draft to reference models in parallel as diff auditors.
|
|
69
|
+
* 3. Applies valid diff hunks onto draft so each auditor produces a refined candidate output.
|
|
70
|
+
* 4. If the draft model fails, returns `{ fallbackToStandard: true }` so the caller runs standard parallel MoA.
|
|
71
|
+
*/
|
|
72
|
+
export async function runSpeculativeMoa(references = [], messages = [], options = {}, callLlm, onProgress) {
|
|
73
|
+
const draftSlot = (options.draftModel?.provider && options.draftModel?.model)
|
|
74
|
+
? options.draftModel
|
|
75
|
+
: references[0]
|
|
76
|
+
|
|
77
|
+
if (!draftSlot || !draftSlot.provider || !draftSlot.model) {
|
|
78
|
+
return { fallbackToStandard: true, reason: 'No valid speculative draft model configured' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const draftLabel = slotLabel(draftSlot)
|
|
82
|
+
if (typeof onProgress === 'function') {
|
|
83
|
+
onProgress(`⚡ *Speculative MoA: Generating fast initial draft with \`${draftLabel}\`...*\n`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const draftStart = Date.now()
|
|
87
|
+
let draftText = ''
|
|
88
|
+
let draftUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const draftRes = await callLlm({
|
|
92
|
+
provider: draftSlot.provider,
|
|
93
|
+
model: draftSlot.model,
|
|
94
|
+
messages,
|
|
95
|
+
systemPrompt: options.systemPrompt,
|
|
96
|
+
temperature: options.temperature ?? 0.3,
|
|
97
|
+
maxTokens: options.maxTokens ?? 4096,
|
|
98
|
+
signal: options.signal,
|
|
99
|
+
})
|
|
100
|
+
draftText = typeof draftRes === 'string' ? draftRes : (draftRes?.content || draftRes?.text || '')
|
|
101
|
+
if (!draftText || !draftText.trim()) {
|
|
102
|
+
throw new Error('Empty draft response from speculative draft model')
|
|
103
|
+
}
|
|
104
|
+
const rawUsage = (typeof draftRes === 'object' && draftRes?.usage) ? draftRes.usage : {
|
|
105
|
+
inputTokens: 100,
|
|
106
|
+
outputTokens: Math.max(1, Math.round(draftText.length / 4)),
|
|
107
|
+
}
|
|
108
|
+
draftUsage = estimateTokenCost(draftSlot, rawUsage, options.prices || {})
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.warn(`[dsh-moa:speculative] Draft model ${draftLabel} failed (${err?.message || err}); falling back to standard parallel MoA.`)
|
|
111
|
+
if (typeof onProgress === 'function') {
|
|
112
|
+
onProgress(`⚠️ *Speculative draft model \`${draftLabel}\` failed; falling back to standard parallel MoA...*\n`)
|
|
113
|
+
}
|
|
114
|
+
return { fallbackToStandard: true, reason: err?.message || String(err) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const draftDurationMs = Date.now() - draftStart
|
|
118
|
+
if (typeof onProgress === 'function') {
|
|
119
|
+
onProgress(`🔍 *Speculative MoA: Dispatching draft to ${references.length} parallel diff auditor(s)...*\n`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const lastUserPrompt = messages[messages.length - 1]?.content || ''
|
|
123
|
+
const auditTasks = references.map(async (slot, idx) => {
|
|
124
|
+
const candidateIndex = idx + 1
|
|
125
|
+
const label = slotLabel(slot)
|
|
126
|
+
const auditStart = Date.now()
|
|
127
|
+
|
|
128
|
+
const auditPrompt = [
|
|
129
|
+
`You are Speculative Auditor #${candidateIndex} (${label}).`,
|
|
130
|
+
`Review the fast draft below for the user task and provide targeted diff fixes (logic bugs, edge cases, types, security) using SEARCH/REPLACE blocks or \`\`\`diff hunks.`,
|
|
131
|
+
`\n### Original User Task:\n${lastUserPrompt}`,
|
|
132
|
+
`\n### Fast Draft Solution (${draftLabel}):\n${draftText}`,
|
|
133
|
+
`\nOutput your audit notes and any SEARCH/REPLACE or \`\`\`diff blocks needed to perfect the draft.`,
|
|
134
|
+
].join('\n')
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const auditRes = await callLlm({
|
|
138
|
+
provider: slot.provider,
|
|
139
|
+
model: slot.model,
|
|
140
|
+
messages: [{ role: 'user', content: auditPrompt }],
|
|
141
|
+
temperature: 0.2,
|
|
142
|
+
maxTokens: Math.min(options.maxTokens ?? 2048, 2048),
|
|
143
|
+
signal: options.signal,
|
|
144
|
+
})
|
|
145
|
+
const auditText = typeof auditRes === 'string' ? auditRes : (auditRes?.content || auditRes?.text || '')
|
|
146
|
+
const rawAuditUsage = (typeof auditRes === 'object' && auditRes?.usage) ? auditRes.usage : {
|
|
147
|
+
inputTokens: Math.max(1, Math.round(auditPrompt.length / 4)),
|
|
148
|
+
outputTokens: Math.max(1, Math.round(auditText.length / 4)),
|
|
149
|
+
}
|
|
150
|
+
const auditCost = estimateTokenCost(slot, rawAuditUsage, options.prices || {})
|
|
151
|
+
|
|
152
|
+
const { patchedText, appliedCount } = applyAuditorPatchHunks(draftText, auditText)
|
|
153
|
+
const combinedText = appliedCount > 0
|
|
154
|
+
? `${patchedText}\n\n#### Speculative Auditor Diff Notes (${label}, ${appliedCount} hunk(s) applied):\n${auditText}`
|
|
155
|
+
: `${draftText}\n\n#### Speculative Auditor Review (${label}):\n${auditText}`
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
index: candidateIndex,
|
|
159
|
+
slot,
|
|
160
|
+
label,
|
|
161
|
+
text: combinedText,
|
|
162
|
+
speculativeAppliedHunks: appliedCount,
|
|
163
|
+
speculativeDraftModel: draftLabel,
|
|
164
|
+
usage: {
|
|
165
|
+
inputTokens: auditCost.inputTokens + (idx === 0 ? draftUsage.inputTokens : 0),
|
|
166
|
+
outputTokens: auditCost.outputTokens + (idx === 0 ? draftUsage.outputTokens : 0),
|
|
167
|
+
totalTokens: auditCost.totalTokens + (idx === 0 ? draftUsage.totalTokens : 0),
|
|
168
|
+
},
|
|
169
|
+
costUsd: Number((auditCost.costUsd + (idx === 0 ? draftUsage.costUsd : 0)).toFixed(6)),
|
|
170
|
+
durationMs: (Date.now() - auditStart) + (idx === 0 ? draftDurationMs : 0),
|
|
171
|
+
ok: true,
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return {
|
|
175
|
+
index: candidateIndex,
|
|
176
|
+
slot,
|
|
177
|
+
label,
|
|
178
|
+
text: draftText,
|
|
179
|
+
error: err?.message || String(err),
|
|
180
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
181
|
+
costUsd: 0,
|
|
182
|
+
durationMs: Date.now() - auditStart,
|
|
183
|
+
ok: false,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const referenceOutputs = await Promise.all(auditTasks)
|
|
189
|
+
return {
|
|
190
|
+
fallbackToStandard: false,
|
|
191
|
+
draftModel: draftLabel,
|
|
192
|
+
draftText,
|
|
193
|
+
referenceOutputs,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multimodal Visual MoA Consensus via dsh-vision-bridge and dsh-live-canvas (#250)
|
|
3
|
+
* Renders UI/Frontend candidates (HTML, React, CSS, SVG) via dsh-live-canvas,
|
|
4
|
+
* captures headless screenshots, and evaluates layout/responsiveness via dsh-vision-bridge.
|
|
5
|
+
* Gracefully degrades to text evaluation when companion plugins are unavailable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { service } from './services.js'
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
import { slotLabel } from './moa-prompts.js'
|
|
11
|
+
|
|
12
|
+
const UI_TASK_REGEX = /\b(html|css|svg|ui|frontend|react|vue|tailwind|layout|landing|page|component|button|modal|responsive|card|dashboard|widget)\b/i
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks whether a prompt or candidate output involves UI/Frontend rendering.
|
|
16
|
+
*/
|
|
17
|
+
export function isVisualUiTask(prompt = '', referenceOutputs = []) {
|
|
18
|
+
if (UI_TASK_REGEX.test(String(prompt || ''))) return true
|
|
19
|
+
for (const ref of referenceOutputs || []) {
|
|
20
|
+
const files = Array.isArray(ref?.files) ? ref.files : []
|
|
21
|
+
if (files.some((f) => /\.(html?|css|svg|jsx|tsx|vue)$/i.test(f.relativePath || f.path || ''))) {
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extracts renderable HTML/SVG preview markup from a candidate output.
|
|
30
|
+
*/
|
|
31
|
+
export function extractRenderableMarkup(candidate = {}) {
|
|
32
|
+
const files = Array.isArray(candidate.files) ? candidate.files : []
|
|
33
|
+
const htmlFile = files.find((f) => /\.(html?|svg)$/i.test(f.relativePath || f.path || ''))
|
|
34
|
+
if (htmlFile?.content) {
|
|
35
|
+
return { title: htmlFile.relativePath || 'index.html', content: htmlFile.content }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cssFile = files.find((f) => /\.css$/i.test(f.relativePath || f.path || ''))
|
|
39
|
+
const jsFile = files.find((f) => /\.(jsx?|tsx?)$/i.test(f.relativePath || f.path || ''))
|
|
40
|
+
if (cssFile || jsFile) {
|
|
41
|
+
const html = [
|
|
42
|
+
'<!DOCTYPE html><html><head><meta charset="utf-8">',
|
|
43
|
+
cssFile ? `<style>${cssFile.content}</style>` : '',
|
|
44
|
+
'</head><body><div id="app">UI Preview</div>',
|
|
45
|
+
'</body></html>',
|
|
46
|
+
].join('\n')
|
|
47
|
+
return { title: 'synthesized-preview.html', content: html }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const text = String(candidate.text || '')
|
|
51
|
+
const fenceMatch = /```(?:html|svg)\s*([\s\S]*?)```/i.exec(text)
|
|
52
|
+
if (fenceMatch) {
|
|
53
|
+
return { title: 'inline-preview.html', content: fenceMatch[1].trim() }
|
|
54
|
+
}
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Evaluates candidates visually using dsh-live-canvas and dsh-vision-bridge.
|
|
60
|
+
* Returns { degraded: true } if companion services are absent so the caller falls back cleanly.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} params
|
|
63
|
+
* @returns {Promise<{
|
|
64
|
+
* degraded: boolean,
|
|
65
|
+
* reason?: string,
|
|
66
|
+
* visualScores: Array<{ candidateIndex: number, label: string, visualScore: number, critique: string, previewUrl: string | null }>,
|
|
67
|
+
* directive: string
|
|
68
|
+
* }>}
|
|
69
|
+
*/
|
|
70
|
+
export async function runVisualMoaConsensus({
|
|
71
|
+
ctx = null,
|
|
72
|
+
liveCanvas = null,
|
|
73
|
+
visionBridge = null,
|
|
74
|
+
userPrompt = '',
|
|
75
|
+
referenceOutputs = [],
|
|
76
|
+
onProgress = null,
|
|
77
|
+
}) {
|
|
78
|
+
const resolvedCanvas = liveCanvas || service(ctx, 'liveCanvas')
|
|
79
|
+
const resolvedVision = visionBridge || service(ctx, 'visionBridge')
|
|
80
|
+
|
|
81
|
+
if (!resolvedCanvas && !resolvedVision) {
|
|
82
|
+
return {
|
|
83
|
+
degraded: true,
|
|
84
|
+
reason: 'Companion plugins dsh-live-canvas and dsh-vision-bridge are not active; using standard text evaluation.',
|
|
85
|
+
visualScores: [],
|
|
86
|
+
directive: '',
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof onProgress === 'function') {
|
|
91
|
+
onProgress('👁️ *Visual MoA Consensus: Rendering UI candidates in Live Canvas & evaluating via Vision Bridge...*\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const visualScores = []
|
|
95
|
+
for (let i = 0; i < referenceOutputs.length; i++) {
|
|
96
|
+
const ref = referenceOutputs[i]
|
|
97
|
+
if (!ref || ref.ok === false) continue
|
|
98
|
+
const idx = ref.index ?? (i + 1)
|
|
99
|
+
const label = ref.label || slotLabel(ref.slot || ref)
|
|
100
|
+
const markup = extractRenderableMarkup(ref)
|
|
101
|
+
if (!markup) continue
|
|
102
|
+
|
|
103
|
+
let previewUrl = null
|
|
104
|
+
let screenshot = null
|
|
105
|
+
|
|
106
|
+
if (resolvedCanvas) {
|
|
107
|
+
try {
|
|
108
|
+
if (typeof resolvedCanvas.createPreviewFromContent === 'function') {
|
|
109
|
+
const prev = await resolvedCanvas.createPreviewFromContent({
|
|
110
|
+
content: markup.content,
|
|
111
|
+
title: `Candidate #${idx} (${label}) - ${markup.title}`,
|
|
112
|
+
})
|
|
113
|
+
previewUrl = prev?.previewUrl || null
|
|
114
|
+
screenshot = prev?.screenshot || prev?.imageBase64 || null
|
|
115
|
+
}
|
|
116
|
+
if (!screenshot && typeof resolvedCanvas.captureScreenshot === 'function') {
|
|
117
|
+
screenshot = await resolvedCanvas.captureScreenshot({
|
|
118
|
+
content: markup.content,
|
|
119
|
+
previewUrl,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logger.warn(`[dsh-moa:visual] Live Canvas preview failed for candidate #${idx}:`, err?.message || err)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let visualScore = 7.5
|
|
128
|
+
let critique = 'Rendered cleanly in Live Canvas preview.'
|
|
129
|
+
|
|
130
|
+
if (resolvedVision) {
|
|
131
|
+
try {
|
|
132
|
+
const evalFn = resolvedVision.evaluateVisualLayout || resolvedVision.analyzeImage || resolvedVision.inspectUi
|
|
133
|
+
if (typeof evalFn === 'function') {
|
|
134
|
+
const vRes = await evalFn.call(resolvedVision, {
|
|
135
|
+
prompt: userPrompt,
|
|
136
|
+
candidateIndex: idx,
|
|
137
|
+
html: markup.content,
|
|
138
|
+
previewUrl,
|
|
139
|
+
screenshot,
|
|
140
|
+
})
|
|
141
|
+
if (vRes) {
|
|
142
|
+
if (Number.isFinite(Number(vRes.score))) {
|
|
143
|
+
visualScore = Math.max(1, Math.min(10, Number(vRes.score)))
|
|
144
|
+
}
|
|
145
|
+
if (vRes.critique || vRes.summary || vRes.feedback) {
|
|
146
|
+
critique = String(vRes.critique || vRes.summary || vRes.feedback).trim()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.warn(`[dsh-moa:visual] Vision Bridge evaluation failed for candidate #${idx}:`, err?.message || err)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
visualScores.push({
|
|
156
|
+
candidateIndex: idx,
|
|
157
|
+
label,
|
|
158
|
+
visualScore: Number(visualScore.toFixed(1)),
|
|
159
|
+
critique,
|
|
160
|
+
previewUrl,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (visualScores.length === 0) {
|
|
165
|
+
return {
|
|
166
|
+
degraded: true,
|
|
167
|
+
reason: 'No renderable HTML/CSS/SVG artifacts found in candidates.',
|
|
168
|
+
visualScores: [],
|
|
169
|
+
directive: '',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const lines = [
|
|
174
|
+
'### 👁️ Multimodal Visual MoA Consensus (`dsh-live-canvas` + `dsh-vision-bridge`):',
|
|
175
|
+
'The following UI candidates were rendered and visually inspected for layout, responsiveness, and design fidelity:',
|
|
176
|
+
...visualScores.map(
|
|
177
|
+
(v) => `- **Candidate #${v.candidateIndex} (${v.label})**: Visual Score **${v.visualScore}/10** — ${v.critique}${v.previewUrl ? ` ([Live Preview](${v.previewUrl}))` : ''}`
|
|
178
|
+
),
|
|
179
|
+
'Prioritize candidates with higher visual layout fidelity and zero rendering defects.',
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
degraded: false,
|
|
184
|
+
visualScores,
|
|
185
|
+
directive: lines.join('\n'),
|
|
186
|
+
}
|
|
187
|
+
}
|
package/lib/moa-workbench.js
CHANGED
|
@@ -39,6 +39,24 @@ export const COMPANION_PLUGINS = [
|
|
|
39
39
|
url: 'https://github.com/GooDAnDReaDY/dsh-subscriptions',
|
|
40
40
|
description: 'Subscription tier and quota tracking for LLM providers',
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
id: 'dsh-vision-bridge',
|
|
44
|
+
name: 'DSH Vision Bridge',
|
|
45
|
+
url: 'https://github.com/GooDAnDReaDY/dsh-vision-bridge',
|
|
46
|
+
description: 'Multimodal VLM visual evaluation for UI/Frontend artifacts',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'dsh-live-canvas',
|
|
50
|
+
name: 'DSH Live Canvas',
|
|
51
|
+
url: 'https://github.com/GooDAnDReaDY/dsh-live-canvas',
|
|
52
|
+
description: 'Isolated live HTML/CSS/SVG preview environment and screenshot capture',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'dsh-context-lens',
|
|
56
|
+
name: 'DSH Context Lens',
|
|
57
|
+
url: 'https://github.com/GooDAnDReaDY/dsh-context-lens',
|
|
58
|
+
description: 'Semantic AST parsing and symbol-level composite code merging',
|
|
59
|
+
},
|
|
42
60
|
]
|
|
43
61
|
|
|
44
62
|
/**
|