skyloom 1.12.0 → 1.13.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/.github/workflows/ci.yml +36 -36
- package/README.md +142 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -155
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/main.js +103 -51
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +8 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/main.ts +425 -372
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +272 -269
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/vitest.config.ts +17 -17
package/src/core/arbitrate.ts
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 多Agent冲突仲裁 — majority voting, quality scoring, tie-breaking.
|
|
3
|
-
*
|
|
4
|
-
* When multiple agents produce conflicting outputs on the same task
|
|
5
|
-
* (or when a reviewer disagrees with an executor), this module
|
|
6
|
-
* provides structured conflict resolution.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { TaskExecutionResult } from "./factory";
|
|
10
|
-
|
|
11
|
-
/* ═══════════════════════════════════════
|
|
12
|
-
Conflict detection
|
|
13
|
-
═══════════════════════════════════════ */
|
|
14
|
-
export interface Conflict {
|
|
15
|
-
taskId: string;
|
|
16
|
-
results: TaskExecutionResult[];
|
|
17
|
-
description: string;
|
|
18
|
-
severity: "low" | "medium" | "high";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Detect if two results conflict based on success status and content overlap. */
|
|
22
|
-
export function detectConflicts(results: TaskExecutionResult[]): Conflict[] {
|
|
23
|
-
const byTask = new Map<string, TaskExecutionResult[]>();
|
|
24
|
-
for (const r of results) { const id = r.id; if (!byTask.has(id)) byTask.set(id, []); byTask.get(id)!.push(r); }
|
|
25
|
-
|
|
26
|
-
const conflicts: Conflict[] = [];
|
|
27
|
-
for (const [id, items] of byTask) {
|
|
28
|
-
if (items.length < 2) continue;
|
|
29
|
-
|
|
30
|
-
const successes = items.filter(r => r.success);
|
|
31
|
-
const failures = items.filter(r => !r.success);
|
|
32
|
-
|
|
33
|
-
// All succeeded — check content divergence
|
|
34
|
-
if (successes.length >= 2) {
|
|
35
|
-
const contents = successes.map(r => (r.content || "").toLowerCase());
|
|
36
|
-
const similarity = pairwiseSimilarity(contents);
|
|
37
|
-
if (similarity < 0.3) {
|
|
38
|
-
conflicts.push({ taskId: id, results: successes, description: "Multiple agents produced divergent successful outputs", severity: "medium" });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Mix of success and failure
|
|
43
|
-
if (successes.length > 0 && failures.length > 0) {
|
|
44
|
-
conflicts.push({ taskId: id, results: items, description: `${successes.length} succeeded, ${failures.length} failed — need tiebreaker`, severity: "medium" });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// All failed
|
|
48
|
-
if (failures.length >= 2 && successes.length === 0) {
|
|
49
|
-
conflicts.push({ taskId: id, results: failures, description: "All agents failed on this task", severity: "high" });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return conflicts;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* ═══════════════════════════════════════
|
|
57
|
-
Content similarity (n-gram Jaccard)
|
|
58
|
-
═══════════════════════════════════════ */
|
|
59
|
-
function pairwiseSimilarity(texts: string[]): number {
|
|
60
|
-
if (texts.length < 2) return 1.0;
|
|
61
|
-
let total = 0;
|
|
62
|
-
let count = 0;
|
|
63
|
-
for (let i = 0; i < texts.length; i++) {
|
|
64
|
-
for (let j = i + 1; j < texts.length; j++) {
|
|
65
|
-
total += ngramJaccard(texts[i], texts[j], 3);
|
|
66
|
-
count++;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return count === 0 ? 0 : total / count;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function ngramJaccard(a: string, b: string, n: number): number {
|
|
73
|
-
const as = ngrams(a, n), bs = ngrams(b, n);
|
|
74
|
-
if (as.size === 0 && bs.size === 0) return 1;
|
|
75
|
-
let intersection = 0;
|
|
76
|
-
for (const g of as) { if (bs.has(g)) intersection++; }
|
|
77
|
-
const union = as.size + bs.size - intersection;
|
|
78
|
-
return union === 0 ? 0 : intersection / union;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function ngrams(s: string, n: number): Set<string> {
|
|
82
|
-
const out = new Set<string>();
|
|
83
|
-
for (let i = 0; i <= s.length - n; i++) out.add(s.slice(i, i + n));
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* ═══════════════════════════════════════
|
|
88
|
-
Majority voting / arbitration
|
|
89
|
-
═══════════════════════════════════════ */
|
|
90
|
-
export interface ArbitrationResult {
|
|
91
|
-
winner: TaskExecutionResult;
|
|
92
|
-
method: "unanimous" | "majority" | "tiebreaker" | "single";
|
|
93
|
-
confidence: number; // 0-1
|
|
94
|
-
reasoning: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Pick the best result from conflicting ones via majority vote. */
|
|
98
|
-
export function arbitrate(results: TaskExecutionResult[]): ArbitrationResult {
|
|
99
|
-
if (results.length === 0) throw new Error("No results to arbitrate");
|
|
100
|
-
if (results.length === 1) return { winner: results[0], method: "single", confidence: 0.8, reasoning: "Only one result available" };
|
|
101
|
-
|
|
102
|
-
const success = results.filter(r => r.success);
|
|
103
|
-
const fail = results.filter(r => !r.success);
|
|
104
|
-
|
|
105
|
-
// All agree (success)
|
|
106
|
-
if (success.length === results.length) {
|
|
107
|
-
const longest = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
|
|
108
|
-
return { winner: longest, method: "unanimous", confidence: 0.95, reasoning: `${results.length}/${results.length} agents agreed` };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Majority success
|
|
112
|
-
if (success.length > fail.length) {
|
|
113
|
-
// Pick the longest successful content (most detailed)
|
|
114
|
-
const best = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
|
|
115
|
-
return { winner: best, method: "majority", confidence: success.length / results.length, reasoning: `${success.length}/${results.length} succeeded, selected most detailed` };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Majority failure — pick the "closest to success" (longest content)
|
|
119
|
-
if (fail.length > success.length) {
|
|
120
|
-
const best = fail.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
|
|
121
|
-
return { winner: best, method: "majority", confidence: 0.3, reasoning: `Majority failed (${fail.length}/${results.length}), best-effort from partial output` };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Tie — prefer success, or longest content
|
|
125
|
-
const tie = success.length > 0 ? success[0] : fail[0];
|
|
126
|
-
return { winner: tie, method: "tiebreaker", confidence: 0.5, reasoning: `Tie — selected ${tie.success ? "success" : "longest"} result` };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/* ═══════════════════════════════════════
|
|
130
|
-
Quality scoring for individual results
|
|
131
|
-
═══════════════════════════════════════ */
|
|
132
|
-
export interface QualityScore {
|
|
133
|
-
score: number; // 0-100
|
|
134
|
-
completeness: number; // how much of the task was addressed
|
|
135
|
-
richness: number; // detail level of the output
|
|
136
|
-
correctness: number; // did it match expectations (requires ground truth)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function scoreQuality(result: TaskExecutionResult): QualityScore {
|
|
140
|
-
const content = result.content || "";
|
|
141
|
-
|
|
142
|
-
// Completeness: length is a weak proxy but useful
|
|
143
|
-
const completeness = content.length > 500 ? 80 : content.length > 100 ? 50 : content.length > 0 ? 20 : 0;
|
|
144
|
-
|
|
145
|
-
// Richness: code blocks, structured output, bullet points
|
|
146
|
-
let richness = 50;
|
|
147
|
-
if (/```/.test(content)) richness += 20;
|
|
148
|
-
if (/\|.*\|.*\|/.test(content)) richness += 15; // tables
|
|
149
|
-
if (/^[-*] /.test(content)) richness += 10; // bullets
|
|
150
|
-
if (/\d+\./.test(content)) richness += 10; // numbered lists
|
|
151
|
-
richness = Math.min(100, richness);
|
|
152
|
-
|
|
153
|
-
// Correctness: basic sanity checks
|
|
154
|
-
let correctness = 70;
|
|
155
|
-
if (content.includes("Error") || content.includes("error")) correctness -= 20;
|
|
156
|
-
if (content.includes("[REDACTED]")) correctness -= 10;
|
|
157
|
-
if (content.includes("truncated")) correctness -= 15;
|
|
158
|
-
correctness = Math.max(0, correctness);
|
|
159
|
-
|
|
160
|
-
const score = Math.round((completeness * 0.3 + richness * 0.3 + correctness * 0.4));
|
|
161
|
-
return { score, completeness, richness, correctness };
|
|
162
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 多Agent冲突仲裁 — majority voting, quality scoring, tie-breaking.
|
|
3
|
+
*
|
|
4
|
+
* When multiple agents produce conflicting outputs on the same task
|
|
5
|
+
* (or when a reviewer disagrees with an executor), this module
|
|
6
|
+
* provides structured conflict resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TaskExecutionResult } from "./factory";
|
|
10
|
+
|
|
11
|
+
/* ═══════════════════════════════════════
|
|
12
|
+
Conflict detection
|
|
13
|
+
═══════════════════════════════════════ */
|
|
14
|
+
export interface Conflict {
|
|
15
|
+
taskId: string;
|
|
16
|
+
results: TaskExecutionResult[];
|
|
17
|
+
description: string;
|
|
18
|
+
severity: "low" | "medium" | "high";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Detect if two results conflict based on success status and content overlap. */
|
|
22
|
+
export function detectConflicts(results: TaskExecutionResult[]): Conflict[] {
|
|
23
|
+
const byTask = new Map<string, TaskExecutionResult[]>();
|
|
24
|
+
for (const r of results) { const id = r.id; if (!byTask.has(id)) byTask.set(id, []); byTask.get(id)!.push(r); }
|
|
25
|
+
|
|
26
|
+
const conflicts: Conflict[] = [];
|
|
27
|
+
for (const [id, items] of byTask) {
|
|
28
|
+
if (items.length < 2) continue;
|
|
29
|
+
|
|
30
|
+
const successes = items.filter(r => r.success);
|
|
31
|
+
const failures = items.filter(r => !r.success);
|
|
32
|
+
|
|
33
|
+
// All succeeded — check content divergence
|
|
34
|
+
if (successes.length >= 2) {
|
|
35
|
+
const contents = successes.map(r => (r.content || "").toLowerCase());
|
|
36
|
+
const similarity = pairwiseSimilarity(contents);
|
|
37
|
+
if (similarity < 0.3) {
|
|
38
|
+
conflicts.push({ taskId: id, results: successes, description: "Multiple agents produced divergent successful outputs", severity: "medium" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Mix of success and failure
|
|
43
|
+
if (successes.length > 0 && failures.length > 0) {
|
|
44
|
+
conflicts.push({ taskId: id, results: items, description: `${successes.length} succeeded, ${failures.length} failed — need tiebreaker`, severity: "medium" });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// All failed
|
|
48
|
+
if (failures.length >= 2 && successes.length === 0) {
|
|
49
|
+
conflicts.push({ taskId: id, results: failures, description: "All agents failed on this task", severity: "high" });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return conflicts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ═══════════════════════════════════════
|
|
57
|
+
Content similarity (n-gram Jaccard)
|
|
58
|
+
═══════════════════════════════════════ */
|
|
59
|
+
function pairwiseSimilarity(texts: string[]): number {
|
|
60
|
+
if (texts.length < 2) return 1.0;
|
|
61
|
+
let total = 0;
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (let i = 0; i < texts.length; i++) {
|
|
64
|
+
for (let j = i + 1; j < texts.length; j++) {
|
|
65
|
+
total += ngramJaccard(texts[i], texts[j], 3);
|
|
66
|
+
count++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return count === 0 ? 0 : total / count;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ngramJaccard(a: string, b: string, n: number): number {
|
|
73
|
+
const as = ngrams(a, n), bs = ngrams(b, n);
|
|
74
|
+
if (as.size === 0 && bs.size === 0) return 1;
|
|
75
|
+
let intersection = 0;
|
|
76
|
+
for (const g of as) { if (bs.has(g)) intersection++; }
|
|
77
|
+
const union = as.size + bs.size - intersection;
|
|
78
|
+
return union === 0 ? 0 : intersection / union;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ngrams(s: string, n: number): Set<string> {
|
|
82
|
+
const out = new Set<string>();
|
|
83
|
+
for (let i = 0; i <= s.length - n; i++) out.add(s.slice(i, i + n));
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ═══════════════════════════════════════
|
|
88
|
+
Majority voting / arbitration
|
|
89
|
+
═══════════════════════════════════════ */
|
|
90
|
+
export interface ArbitrationResult {
|
|
91
|
+
winner: TaskExecutionResult;
|
|
92
|
+
method: "unanimous" | "majority" | "tiebreaker" | "single";
|
|
93
|
+
confidence: number; // 0-1
|
|
94
|
+
reasoning: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Pick the best result from conflicting ones via majority vote. */
|
|
98
|
+
export function arbitrate(results: TaskExecutionResult[]): ArbitrationResult {
|
|
99
|
+
if (results.length === 0) throw new Error("No results to arbitrate");
|
|
100
|
+
if (results.length === 1) return { winner: results[0], method: "single", confidence: 0.8, reasoning: "Only one result available" };
|
|
101
|
+
|
|
102
|
+
const success = results.filter(r => r.success);
|
|
103
|
+
const fail = results.filter(r => !r.success);
|
|
104
|
+
|
|
105
|
+
// All agree (success)
|
|
106
|
+
if (success.length === results.length) {
|
|
107
|
+
const longest = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
|
|
108
|
+
return { winner: longest, method: "unanimous", confidence: 0.95, reasoning: `${results.length}/${results.length} agents agreed` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Majority success
|
|
112
|
+
if (success.length > fail.length) {
|
|
113
|
+
// Pick the longest successful content (most detailed)
|
|
114
|
+
const best = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
|
|
115
|
+
return { winner: best, method: "majority", confidence: success.length / results.length, reasoning: `${success.length}/${results.length} succeeded, selected most detailed` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Majority failure — pick the "closest to success" (longest content)
|
|
119
|
+
if (fail.length > success.length) {
|
|
120
|
+
const best = fail.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
|
|
121
|
+
return { winner: best, method: "majority", confidence: 0.3, reasoning: `Majority failed (${fail.length}/${results.length}), best-effort from partial output` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Tie — prefer success, or longest content
|
|
125
|
+
const tie = success.length > 0 ? success[0] : fail[0];
|
|
126
|
+
return { winner: tie, method: "tiebreaker", confidence: 0.5, reasoning: `Tie — selected ${tie.success ? "success" : "longest"} result` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ═══════════════════════════════════════
|
|
130
|
+
Quality scoring for individual results
|
|
131
|
+
═══════════════════════════════════════ */
|
|
132
|
+
export interface QualityScore {
|
|
133
|
+
score: number; // 0-100
|
|
134
|
+
completeness: number; // how much of the task was addressed
|
|
135
|
+
richness: number; // detail level of the output
|
|
136
|
+
correctness: number; // did it match expectations (requires ground truth)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function scoreQuality(result: TaskExecutionResult): QualityScore {
|
|
140
|
+
const content = result.content || "";
|
|
141
|
+
|
|
142
|
+
// Completeness: length is a weak proxy but useful
|
|
143
|
+
const completeness = content.length > 500 ? 80 : content.length > 100 ? 50 : content.length > 0 ? 20 : 0;
|
|
144
|
+
|
|
145
|
+
// Richness: code blocks, structured output, bullet points
|
|
146
|
+
let richness = 50;
|
|
147
|
+
if (/```/.test(content)) richness += 20;
|
|
148
|
+
if (/\|.*\|.*\|/.test(content)) richness += 15; // tables
|
|
149
|
+
if (/^[-*] /.test(content)) richness += 10; // bullets
|
|
150
|
+
if (/\d+\./.test(content)) richness += 10; // numbered lists
|
|
151
|
+
richness = Math.min(100, richness);
|
|
152
|
+
|
|
153
|
+
// Correctness: basic sanity checks
|
|
154
|
+
let correctness = 70;
|
|
155
|
+
if (content.includes("Error") || content.includes("error")) correctness -= 20;
|
|
156
|
+
if (content.includes("[REDACTED]")) correctness -= 10;
|
|
157
|
+
if (content.includes("truncated")) correctness -= 15;
|
|
158
|
+
correctness = Math.max(0, correctness);
|
|
159
|
+
|
|
160
|
+
const score = Math.round((completeness * 0.3 + richness * 0.3 + correctness * 0.4));
|
|
161
|
+
return { score, completeness, richness, correctness };
|
|
162
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model & Provider Catalog — single source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the three drifting copies of model data (config/models.yaml,
|
|
5
|
+
* the hardcoded setup-wizard list in cli/main.ts, and the README table)
|
|
6
|
+
* with one typed, validated catalog loaded from config/models.yaml.
|
|
7
|
+
*
|
|
8
|
+
* Every model exposed here is intended to be directly callable. Fictional
|
|
9
|
+
* or unreleased models must not appear in models.yaml.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as yaml from "yaml";
|
|
15
|
+
import { CONFIG_DIR } from "./config";
|
|
16
|
+
import { getLogger } from "./logger";
|
|
17
|
+
|
|
18
|
+
const log = getLogger("catalog");
|
|
19
|
+
|
|
20
|
+
/** A single callable model. Costs are USD per 1M tokens. */
|
|
21
|
+
export interface ModelInfo {
|
|
22
|
+
/** Model id as passed to the provider (e.g. "gpt-4o", "openai/gpt-4.1"). */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Catalog provider key (e.g. "openai", "deepseek"). */
|
|
25
|
+
provider: string;
|
|
26
|
+
/** Context window in tokens. */
|
|
27
|
+
context: number;
|
|
28
|
+
/** Input cost, USD per 1M tokens. */
|
|
29
|
+
costIn: number;
|
|
30
|
+
/** Output cost, USD per 1M tokens. */
|
|
31
|
+
costOut: number;
|
|
32
|
+
/** Short human description. */
|
|
33
|
+
desc: string;
|
|
34
|
+
/** True for local/free providers (ollama, zero-cost). */
|
|
35
|
+
local: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Display + ordering metadata for a provider in setup/UI. */
|
|
39
|
+
export interface ProviderMeta {
|
|
40
|
+
id: string;
|
|
41
|
+
/** Human label shown in the setup wizard. */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Env var that supplies the API key (when applicable). */
|
|
44
|
+
envVar?: string;
|
|
45
|
+
/** Sort order in the wizard. */
|
|
46
|
+
order: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Provider display metadata. Ordering matches the setup wizard.
|
|
51
|
+
* Provider *ids* match the keys used in config/models.yaml.
|
|
52
|
+
*/
|
|
53
|
+
export const PROVIDER_META: Record<string, ProviderMeta> = {
|
|
54
|
+
deepseek: { id: "deepseek", name: "DeepSeek", envVar: "DEEPSEEK_API_KEY", order: 1 },
|
|
55
|
+
openai: { id: "openai", name: "OpenAI", envVar: "OPENAI_API_KEY", order: 2 },
|
|
56
|
+
anthropic: { id: "anthropic", name: "Anthropic", envVar: "ANTHROPIC_API_KEY", order: 3 },
|
|
57
|
+
google: { id: "google", name: "Google Gemini", envVar: "GEMINI_API_KEY", order: 4 },
|
|
58
|
+
groq: { id: "groq", name: "Groq", envVar: "GROQ_API_KEY", order: 5 },
|
|
59
|
+
openrouter: { id: "openrouter", name: "OpenRouter (多模型)", envVar: "OPENROUTER_API_KEY", order: 6 },
|
|
60
|
+
mistral: { id: "mistral", name: "Mistral", envVar: "MISTRAL_API_KEY", order: 7 },
|
|
61
|
+
xai: { id: "xai", name: "xAI (Grok)", envVar: "XAI_API_KEY", order: 8 },
|
|
62
|
+
ollama: { id: "ollama", name: "Ollama 本地", order: 9 },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Raw shape of an entry in config/models.yaml. */
|
|
66
|
+
interface RawModelEntry {
|
|
67
|
+
name: string;
|
|
68
|
+
context?: number;
|
|
69
|
+
cost_in?: number;
|
|
70
|
+
cost_out?: number;
|
|
71
|
+
desc?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let catalogCache: Map<string, ModelInfo[]> | null = null;
|
|
75
|
+
|
|
76
|
+
function readModelsYaml(): Record<string, RawModelEntry[]> {
|
|
77
|
+
const modelPath = path.join(CONFIG_DIR, "models.yaml");
|
|
78
|
+
try {
|
|
79
|
+
if (!fs.existsSync(modelPath)) {
|
|
80
|
+
log.warn("models_yaml_missing", { path: modelPath });
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
const data = yaml.parse(fs.readFileSync(modelPath, "utf-8"));
|
|
84
|
+
return (data && typeof data === "object" ? data : {}) as Record<string, RawModelEntry[]>;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
log.error("models_yaml_parse_failed", { error: String(e) });
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Load (and cache) the full catalog as provider -> models. */
|
|
92
|
+
export function loadCatalog(): Map<string, ModelInfo[]> {
|
|
93
|
+
if (catalogCache) return catalogCache;
|
|
94
|
+
|
|
95
|
+
const raw = readModelsYaml();
|
|
96
|
+
const catalog = new Map<string, ModelInfo[]>();
|
|
97
|
+
|
|
98
|
+
for (const [provider, entries] of Object.entries(raw)) {
|
|
99
|
+
if (!Array.isArray(entries)) continue;
|
|
100
|
+
const models: ModelInfo[] = [];
|
|
101
|
+
for (const e of entries) {
|
|
102
|
+
if (!e || typeof e.name !== "string") continue;
|
|
103
|
+
const costIn = e.cost_in ?? 0;
|
|
104
|
+
const costOut = e.cost_out ?? 0;
|
|
105
|
+
models.push({
|
|
106
|
+
id: e.name,
|
|
107
|
+
provider,
|
|
108
|
+
context: e.context ?? 0,
|
|
109
|
+
costIn,
|
|
110
|
+
costOut,
|
|
111
|
+
desc: e.desc ?? "",
|
|
112
|
+
local: provider === "ollama" || (costIn === 0 && costOut === 0),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (models.length > 0) catalog.set(provider, models);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
catalogCache = catalog;
|
|
119
|
+
return catalog;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Clear the cache (used by tests). */
|
|
123
|
+
export function resetCatalogCache(): void {
|
|
124
|
+
catalogCache = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** All provider ids present in the catalog, in wizard order. */
|
|
128
|
+
export function listProviders(): string[] {
|
|
129
|
+
const present = [...loadCatalog().keys()];
|
|
130
|
+
return present.sort((a, b) => (PROVIDER_META[a]?.order ?? 99) - (PROVIDER_META[b]?.order ?? 99));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Models for a provider (empty array if unknown). */
|
|
134
|
+
export function modelsFor(provider: string): ModelInfo[] {
|
|
135
|
+
return loadCatalog().get(provider) ?? [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Flat list of every callable model across providers. */
|
|
139
|
+
export function allModels(): ModelInfo[] {
|
|
140
|
+
return [...loadCatalog().values()].flat();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Look up a model by id. Matching is tolerant of a "provider/" prefix
|
|
145
|
+
* (e.g. "openai/gpt-4o" resolves to the "gpt-4o" entry under openai).
|
|
146
|
+
*/
|
|
147
|
+
export function getModelInfo(modelId: string): ModelInfo | null {
|
|
148
|
+
const all = allModels();
|
|
149
|
+
const exact = all.find((m) => m.id === modelId);
|
|
150
|
+
if (exact) return exact;
|
|
151
|
+
// tolerate provider/ prefix on either side
|
|
152
|
+
const stripped = modelId.includes("/") ? modelId.split("/").slice(1).join("/") : modelId;
|
|
153
|
+
return all.find((m) => m.id === stripped || m.id.split("/").slice(1).join("/") === modelId) ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Whether a model id is a known, callable model in the catalog. */
|
|
157
|
+
export function isKnownModel(modelId: string): boolean {
|
|
158
|
+
return getModelInfo(modelId) !== null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Provider display label (falls back to the raw id). */
|
|
162
|
+
export function providerLabel(provider: string): string {
|
|
163
|
+
return PROVIDER_META[provider]?.name ?? provider;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validate that a configured default model is callable. Returns a short
|
|
168
|
+
* list of suggested model ids when it is not, so callers can fail loudly
|
|
169
|
+
* instead of 404-ing at request time.
|
|
170
|
+
*/
|
|
171
|
+
export function validateModel(modelId: string | undefined): { ok: boolean; suggestions: string[] } {
|
|
172
|
+
if (modelId && isKnownModel(modelId)) return { ok: true, suggestions: [] };
|
|
173
|
+
const suggestions = allModels()
|
|
174
|
+
.filter((m) => !m.local)
|
|
175
|
+
.slice(0, 6)
|
|
176
|
+
.map((m) => m.id);
|
|
177
|
+
return { ok: false, suggestions };
|
|
178
|
+
}
|