@akemi-mio/creativity 0.0.0-stage → 0.1.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.
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HypothesisGenerator = void 0;
4
+ const CreativityPrompt_1 = require("./CreativityPrompt");
5
+ const TemplateLibrary_1 = require("./TemplateLibrary");
6
+ const LocalModelService_1 = require("./LocalModelService");
7
+ const random_1 = require("@akemi-mio/core/utils/random");
8
+ const Logger_1 = require("@akemi-mio/core/logger/Logger");
9
+ /**
10
+ * HypothesisGenerator — 将概念组合转化为可验证的假设
11
+ *
12
+ * 四级生成链(逐级降级):
13
+ * 远程 LLM → 本地模型 (transformers.js) → TemplateLibrary (30+ 模式) → 原始模板兜底
14
+ */
15
+ class HypothesisGenerator {
16
+ idCounter = 0;
17
+ rng;
18
+ chatJson;
19
+ templateLib;
20
+ localModel;
21
+ rotationSeed = 0;
22
+ callCount = 0;
23
+ constructor(chatJson, seed, rotationSeed = 0) {
24
+ this.chatJson = chatJson;
25
+ this.rng = (0, random_1.resolveRandom)(seed);
26
+ this.templateLib = new TemplateLibrary_1.TemplateLibrary(seed);
27
+ this.localModel = new LocalModelService_1.LocalModelService();
28
+ this.rotationSeed = rotationSeed;
29
+ }
30
+ /**
31
+ * 从概念组合生成假设 — 远程 LLM → 本地模型 → TemplateLibrary → 模板兜底
32
+ *
33
+ * 四级 fallback 链:
34
+ * 1. 远程 LLM (高质,但 30s 超时、网络依赖)
35
+ * 2. 本地 transformers.js (进程内,~1s,无网络依赖)
36
+ * 3. TemplateLibrary (30+ 模式,类型感知,零延迟)
37
+ * 4. 原始模板 (5 个硬编码,终极兜底)
38
+ */
39
+ async generate(combos, sources,
40
+ /** 梦境模式使用更高温度 */
41
+ dreamMode = false,
42
+ /** 外部信号 — 不参与配对,作为审视视角注入 LLM */
43
+ externalSignals = [],
44
+ /** 显式技法;缺省走五技法轮换(rotationSeed + 调用次数) */
45
+ method) {
46
+ if (combos.length === 0)
47
+ return [];
48
+ const technique = method ?? (0, CreativityPrompt_1.pickTechnique)(this.rotationSeed + this.callCount);
49
+ this.callCount += 1;
50
+ // 第一级:远程 LLM 驱动生成
51
+ const llmResults = await this.tryLLM(sources, combos, dreamMode, externalSignals, technique);
52
+ if (llmResults.length > 0) {
53
+ return this.tag(llmResults.map((r) => ({
54
+ id: `hyp_${Date.now()}_${++this.idCounter}_${this.rng().toString(36).slice(2, 4)}`,
55
+ title: r.title,
56
+ idea: r.idea,
57
+ expectedBenefit: r.expectedBenefit,
58
+ risk: r.risk,
59
+ sourceLabels: r.sourceLabels,
60
+ sourceDetails: this.buildSourceDetails(r.sourceLabels, sources),
61
+ novelty: Math.max(10, Math.min(100, r.novelty)),
62
+ feasibility: Math.max(10, Math.min(100, r.feasibility)),
63
+ impact: Math.max(10, Math.min(100, r.impact)),
64
+ status: 'draft',
65
+ createdAt: Date.now(),
66
+ })), technique);
67
+ }
68
+ // 第二级:本地 transformers.js 模型
69
+ if (this.localModel.isEnabled) {
70
+ (0, Logger_1.log)('INFO', 'hypothesis_fallback_local', { count: combos.length });
71
+ const localResults = await this.tryLocalModel(sources, combos, dreamMode, technique);
72
+ if (localResults.length > 0) {
73
+ return this.tag(localResults.map((r) => ({
74
+ id: `hyp_local_${Date.now()}_${++this.idCounter}_${this.rng().toString(36).slice(2, 4)}`,
75
+ title: r.title,
76
+ idea: r.idea,
77
+ expectedBenefit: r.expectedBenefit,
78
+ risk: r.risk,
79
+ sourceLabels: r.sourceLabels,
80
+ sourceDetails: this.buildSourceDetails(r.sourceLabels, sources),
81
+ novelty: Math.max(10, Math.min(100, r.novelty)),
82
+ feasibility: Math.max(10, Math.min(100, r.feasibility)),
83
+ impact: Math.max(10, Math.min(100, r.impact)),
84
+ status: 'draft',
85
+ createdAt: Date.now(),
86
+ })), technique);
87
+ }
88
+ }
89
+ // 第二/三级失败 → 记录日志
90
+ (0, Logger_1.log)('INFO', 'hypothesis_fallback_templates', { count: combos.length });
91
+ // 第三级:TemplateLibrary (30+ 类型感知模板)
92
+ const tplResults = [];
93
+ for (const combo of combos) {
94
+ const h = this.templateLib.generate(combo, sources);
95
+ if (h)
96
+ tplResults.push(h);
97
+ }
98
+ if (tplResults.length > 0) {
99
+ return this.tag(tplResults, technique);
100
+ }
101
+ // 第四级:原始模板兜底
102
+ (0, Logger_1.log)('WARN', 'hypothesis_fallback_legacy', { count: combos.length });
103
+ return this.tag(combos.map((combo) => this.templateFallback(combo)), technique);
104
+ }
105
+ /**
106
+ * 调 LLM 生成创意
107
+ */
108
+ async tryLLM(sources, combos, dreamMode, externalSignals = [], technique = '') {
109
+ // 限制输入量,避免超 token
110
+ const topCombos = combos.slice(0, dreamMode ? 5 : 3);
111
+ const comboInfo = topCombos.map((c) => ({
112
+ sources: c.sources,
113
+ description: c.description,
114
+ }));
115
+ const prompt = (0, CreativityPrompt_1.buildCreativityPrompt)(sources, comboInfo, externalSignals);
116
+ try {
117
+ const result = await this.chatJson(prompt, {
118
+ system: (0, CreativityPrompt_1.buildSystemPrompt)(technique),
119
+ temperature: dreamMode ? 1.0 : 0.8,
120
+ timeoutMs: 60000,
121
+ // 输出无界会顶满超时(M2):生成上限 1200 tokens
122
+ maxTokens: 1200,
123
+ });
124
+ if (result.error) {
125
+ (0, Logger_1.log)('WARN', 'hypothesis_llm_error', { error: result.error });
126
+ return [];
127
+ }
128
+ const ideas = Array.isArray(result.data) ? result.data : [];
129
+ // 验证基本格式
130
+ return ideas.filter((i) => typeof i.title === 'string' &&
131
+ typeof i.idea === 'string' &&
132
+ Array.isArray(i.sourceLabels) &&
133
+ typeof i.novelty === 'number' &&
134
+ this.hasRelevanceJustification(i));
135
+ }
136
+ catch (err) {
137
+ (0, Logger_1.log)('ERROR', 'hypothesis_llm_exception', { error: String(err) });
138
+ return [];
139
+ }
140
+ }
141
+ /**
142
+ * 调本地 transformers.js 模型生成创意
143
+ */
144
+ async tryLocalModel(sources, combos, dreamMode, technique = '') {
145
+ const topCombos = combos.slice(0, dreamMode ? 3 : 2);
146
+ const comboInfo = topCombos.map((c) => ({
147
+ sources: c.sources,
148
+ description: c.description,
149
+ }));
150
+ const prompt = (0, CreativityPrompt_1.buildCreativityPrompt)(sources, comboInfo);
151
+ const result = await this.localModel.generate(prompt, {
152
+ system: (0, CreativityPrompt_1.buildSystemPrompt)(technique),
153
+ temperature: dreamMode ? 0.8 : 0.6,
154
+ maxTokens: dreamMode ? 1024 : 768,
155
+ });
156
+ if (result.error) {
157
+ (0, Logger_1.log)('WARN', 'hypothesis_local_error', { error: result.error });
158
+ return [];
159
+ }
160
+ const ideas = Array.isArray(result.data) ? result.data : [];
161
+ return ideas.filter((i) => typeof i.title === 'string' &&
162
+ typeof i.idea === 'string' &&
163
+ Array.isArray(i.sourceLabels) &&
164
+ typeof i.novelty === 'number' &&
165
+ this.hasRelevanceJustification(i));
166
+ }
167
+ hasRelevanceJustification(i) {
168
+ const relevance = typeof i.relevance === 'string' ? i.relevance.trim() : '';
169
+ return relevance.length >= 10;
170
+ }
171
+ /** 记录本轮使用的轮换技法(idea.generate 的 provenance.technique 字段写它)。 */
172
+ tag(hypotheses, technique) {
173
+ return hypotheses.map((h) => ({ ...h, technique }));
174
+ }
175
+ /** 根据 sourceLabels 构建来源深度详情(默认 shallow) */
176
+ buildSourceDetails(labels, sources) {
177
+ const byName = new Map(sources.map((s) => [s.name, s.sourceDepth ?? 'shallow']));
178
+ return labels.map((name) => ({ name, depth: byName.get(name) ?? 'shallow' }));
179
+ }
180
+ /**
181
+ * 模板 fallback — 从旧实现保留
182
+ */
183
+ templateFallback(combo) {
184
+ const [a, b] = combo.sources;
185
+ const hash = this.stableHash(a, b);
186
+ const idx = hash % TEMPLATES.length;
187
+ const t = TEMPLATES[idx];
188
+ return {
189
+ id: `hyp_fb_${Date.now()}_${++this.idCounter}_${this.rng().toString(36).slice(2, 4)}`,
190
+ title: t.title(a, b),
191
+ idea: t.idea(a, b),
192
+ expectedBenefit: t.expectedBenefit,
193
+ risk: t.risk,
194
+ sourceLabels: [a, b],
195
+ sourceDetails: [a, b].map((name) => ({ name, depth: 'shallow' })),
196
+ novelty: 50 + Math.round(this.rng() * 30),
197
+ feasibility: 40 + Math.round(this.rng() * 30),
198
+ impact: 50 + Math.round(this.rng() * 25),
199
+ status: 'draft',
200
+ createdAt: Date.now(),
201
+ };
202
+ }
203
+ stableHash(a, b) {
204
+ let h = 0;
205
+ const s = a + '|' + b;
206
+ for (let i = 0; i < s.length; i++) {
207
+ h = (h << 5) - h + s.charCodeAt(i);
208
+ h = h & h;
209
+ }
210
+ return Math.abs(h);
211
+ }
212
+ }
213
+ exports.HypothesisGenerator = HypothesisGenerator;
214
+ const TEMPLATES = [
215
+ {
216
+ title: (a, b) => `${a} 驱动的 ${b}`,
217
+ idea: (a, b) => `将「${a}」的核心机制作为「${b}」的新输入维度`,
218
+ expectedBenefit: '解锁之前被忽视的新能力组合',
219
+ risk: '组合可能引入不必要的复杂度',
220
+ },
221
+ {
222
+ title: (a, b) => `基于 ${a} 的 ${b} 增强`,
223
+ idea: (a, b) => `让「${a}」和「${b}」通过一个共享接口互相增强`,
224
+ expectedBenefit: '系统灵活性提升,涌现新的行为模式',
225
+ risk: '两个概念耦合后难以单独演进',
226
+ },
227
+ {
228
+ title: (a, b) => `${a} × ${b} 混合系统`,
229
+ idea: (a, b) => `借鉴「${a}」的设计哲学重新思考「${b}」的实现`,
230
+ expectedBenefit: '减少重复逻辑,提升模块间信息复用',
231
+ risk: '可能存在隐含的语义冲突',
232
+ },
233
+ {
234
+ title: (a, b) => `从 ${a} 到 ${b} 的抽象跳跃`,
235
+ idea: (a, b) => `在「${a}」的基础上构建「${b}」的新抽象层`,
236
+ expectedBenefit: '降低认知负载,统一概念模型',
237
+ risk: '抽象层过多影响 runtime 性能',
238
+ },
239
+ {
240
+ title: (a, b) => `${a} 视角下的 ${b} 重构`,
241
+ idea: (a, b) => `将「${a}」的某一种能力移植到「${b}」的上下文中`,
242
+ expectedBenefit: '架构更优雅,扩展点增加',
243
+ risk: '预期收益不确定,需要实验验证',
244
+ },
245
+ ];
246
+ // 保留旧文件中的 PATTERN_LIBRARY 作为参考,但不再使用
@@ -0,0 +1,51 @@
1
+ import type { CreativitySource, ConceptCombo, Hypothesis, CreativeIdea, Strategy, ExternalSignal } from './types';
2
+ /**
3
+ * IdeaGenerator — 创造力引擎的核心编排器
4
+ *
5
+ * 流程:
6
+ * 1. Source Gathering — 从多个来源收集"概念"
7
+ * 2. Concept Mixing — 随机配对重组(规则引擎,保留)
8
+ * 3. Hypothesis Generation — 将组合转化为假设(LLM 驱动,new)
9
+ * 4. Experiment Planning — 为假设生成验证方案(规则引擎,保留)
10
+ */
11
+ export declare class IdeaGenerator {
12
+ private mixer;
13
+ private hypothesisGen;
14
+ private experimentPlanner;
15
+ private rng;
16
+ private temperature;
17
+ /** 已探索过的配对 key 列表,传给 ConceptMixer 以降权 */
18
+ private exploredPairs;
19
+ constructor(chatJson: (userText: string, options?: {
20
+ system?: string;
21
+ temperature?: number;
22
+ timeoutMs?: number;
23
+ requestId?: string;
24
+ }) => Promise<{
25
+ data?: any;
26
+ error?: string;
27
+ }>, temperature?: number, seed?: number, rotationSeed?: number);
28
+ /**
29
+ * 完整一轮"灵感涌现"流程
30
+ * @param strategy 生成策略,约束 ConceptMixer 配对空间
31
+ */
32
+ generateIdeas(sources: CreativitySource[], maxIdeas?: number, strategy?: Strategy, externalSignals?: ExternalSignal[], method?: string): Promise<CreativeIdea[]>;
33
+ /**
34
+ * Dream Mode — 高随机性、跨时间跨度的"梦境"模式
35
+ */
36
+ dreamIdeas(recentSources: CreativitySource[], historicalCombos: ConceptCombo[], failedHypotheses: Hypothesis[], maxIdeas?: number, strategy?: Strategy): Promise<CreativeIdea[]>;
37
+ setTemperature(t: number): void;
38
+ /** 设置已探索过的配对,用于 ConceptMixer 降权 */
39
+ setExploredPairs(pairs: string[]): void;
40
+ /**
41
+ * 可行性/质量门禁 — 过滤明显不靠谱的想法
42
+ * - 可行性评分 >= 30 才保留
43
+ * - 想法描述至少 20 个字符(排除模板填空)
44
+ * - novelty > 80 但 feasibility < 40 的"可疑高新颖性"需要额外检查描述长度
45
+ */
46
+ private feasibilityGate;
47
+ /**
48
+ * 温度影响来源选择权重
49
+ */
50
+ private applyTemperature;
51
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdeaGenerator = void 0;
4
+ const ConceptMixer_1 = require("./ConceptMixer");
5
+ const HypothesisGenerator_1 = require("./HypothesisGenerator");
6
+ const ExperimentPlanner_1 = require("./ExperimentPlanner");
7
+ const random_1 = require("@akemi-mio/core/utils/random");
8
+ /**
9
+ * IdeaGenerator — 创造力引擎的核心编排器
10
+ *
11
+ * 流程:
12
+ * 1. Source Gathering — 从多个来源收集"概念"
13
+ * 2. Concept Mixing — 随机配对重组(规则引擎,保留)
14
+ * 3. Hypothesis Generation — 将组合转化为假设(LLM 驱动,new)
15
+ * 4. Experiment Planning — 为假设生成验证方案(规则引擎,保留)
16
+ */
17
+ class IdeaGenerator {
18
+ mixer;
19
+ hypothesisGen;
20
+ experimentPlanner;
21
+ rng;
22
+ // 创造力温度:越高越随机,越低越保守
23
+ temperature = 0.3;
24
+ /** 已探索过的配对 key 列表,传给 ConceptMixer 以降权 */
25
+ exploredPairs = [];
26
+ constructor(chatJson, temperature = 0.3, seed, rotationSeed = 0) {
27
+ this.rng = (0, random_1.resolveRandom)(seed);
28
+ this.mixer = new ConceptMixer_1.ConceptMixer(seed);
29
+ this.hypothesisGen = new HypothesisGenerator_1.HypothesisGenerator(chatJson, seed, rotationSeed);
30
+ this.experimentPlanner = new ExperimentPlanner_1.ExperimentPlanner(seed);
31
+ this.temperature = temperature;
32
+ }
33
+ /**
34
+ * 完整一轮"灵感涌现"流程
35
+ * @param strategy 生成策略,约束 ConceptMixer 配对空间
36
+ */
37
+ async generateIdeas(sources, maxIdeas = 5, strategy = 'explore', externalSignals = [], method) {
38
+ if (sources.length < 2)
39
+ return [];
40
+ // 1. 随机扰动:温度越高,来源选择越随机
41
+ const activeSources = this.applyTemperature(sources);
42
+ // 2. 概念重组:按策略约束配对 + 打分
43
+ const scoredCombos = this.mixer.mix(activeSources, maxIdeas * 3, this.exploredPairs, strategy);
44
+ const combos = scoredCombos.map((c) => c.combo);
45
+ if (combos.length === 0)
46
+ return [];
47
+ // 3. 用 LLM 生成假设(外部信号作为审视视角注入,不参与配对)
48
+ const hypotheses = await this.hypothesisGen.generate(combos, activeSources, false, externalSignals, method);
49
+ // 4. 可行性/质量门禁(与 dream 模式同一套门槛)——此前只有梦境路径
50
+ // 过 feasibilityGate,主路径仅按 novelty 筛,可行性 0 分也能入库。
51
+ const feasible = this.feasibilityGate(hypotheses);
52
+ // 5. 策略感知的新颖性门禁 — stable 模式同类型配对新颖度天然偏低,
53
+ // 降低阈值避免误杀;signal 模式需要更高的新颖度才有价值
54
+ const noveltyThreshold = strategy === 'stable' ? 30 : strategy === 'signal' ? 40 : 35;
55
+ const novel = feasible.filter((h) => h.novelty >= noveltyThreshold);
56
+ // 6. 为每个假设生成实验方案
57
+ const ideas = novel.slice(0, maxIdeas).map((h) => ({
58
+ hypothesis: h,
59
+ experiment: this.experimentPlanner.plan(h),
60
+ }));
61
+ return ideas;
62
+ }
63
+ /**
64
+ * Dream Mode — 高随机性、跨时间跨度的"梦境"模式
65
+ */
66
+ async dreamIdeas(recentSources, historicalCombos, failedHypotheses, maxIdeas = 3, strategy = 'explore') {
67
+ // 梦境模式:提高温度,纳入失败历史,强制跨类型组合
68
+ const dreamTemperature = 0.8;
69
+ // 将失败方案转化为来源(高权重)
70
+ const failureSources = failedHypotheses.map((h) => ({
71
+ name: `失败:${h.title}`,
72
+ content: `${h.idea}\n风险:${h.risk}`,
73
+ type: 'failure',
74
+ weight: 0.8,
75
+ }));
76
+ const allSources = [
77
+ ...recentSources,
78
+ ...failureSources,
79
+ // 添加随机扰动源
80
+ {
81
+ name: `随机种子_${Date.now()}`,
82
+ content: this.rng().toString(36),
83
+ type: 'random',
84
+ weight: 0.3,
85
+ },
86
+ ];
87
+ // 用高温度混合
88
+ const activeSources = this.mixer.pickRandomSources(allSources, dreamTemperature, 4);
89
+ const scoredCombos = this.mixer.mix(activeSources, maxIdeas * 5, [], strategy);
90
+ const combos = scoredCombos.map((c) => c.combo);
91
+ if (combos.length === 0)
92
+ return [];
93
+ // 梦境模式:高温度 + dreamMode=true
94
+ const hypotheses = await this.hypothesisGen.generate(combos, activeSources, true);
95
+ const feasible = this.feasibilityGate(hypotheses);
96
+ const novel = feasible.filter((h) => h.novelty >= 50);
97
+ return novel.slice(0, maxIdeas).map((h) => ({
98
+ hypothesis: h,
99
+ experiment: this.experimentPlanner.plan(h),
100
+ }));
101
+ }
102
+ setTemperature(t) {
103
+ this.temperature = Math.max(0, Math.min(1, t));
104
+ }
105
+ /** 设置已探索过的配对,用于 ConceptMixer 降权 */
106
+ setExploredPairs(pairs) {
107
+ this.exploredPairs = pairs;
108
+ }
109
+ /**
110
+ * 可行性/质量门禁 — 过滤明显不靠谱的想法
111
+ * - 可行性评分 >= 30 才保留
112
+ * - 想法描述至少 20 个字符(排除模板填空)
113
+ * - novelty > 80 但 feasibility < 40 的"可疑高新颖性"需要额外检查描述长度
114
+ */
115
+ feasibilityGate(hypotheses) {
116
+ return hypotheses.filter((h) => {
117
+ if (h.feasibility < 30)
118
+ return false;
119
+ if (!h.idea || h.idea.length < 20)
120
+ return false;
121
+ if (h.novelty > 80 && h.feasibility < 40) {
122
+ if (!h.idea || h.idea.length < 60)
123
+ return false;
124
+ }
125
+ return true;
126
+ });
127
+ }
128
+ /**
129
+ * 温度影响来源选择权重
130
+ */
131
+ applyTemperature(sources) {
132
+ if (this.temperature < 0.2) {
133
+ return [...sources].sort((a, b) => b.weight - a.weight).slice(0, 4);
134
+ }
135
+ if (this.temperature > 0.7) {
136
+ return sources.filter(() => this.rng() > 0.2);
137
+ }
138
+ return sources.filter((s) => {
139
+ if (s.weight > 0.7)
140
+ return true;
141
+ return this.rng() < 0.85;
142
+ });
143
+ }
144
+ }
145
+ exports.IdeaGenerator = IdeaGenerator;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * LocalModelService — 用 transformers.js 在进程内跑小模型
3
+ *
4
+ * 零外部依赖,进程内运行,不需要 Ollama/Python/独立服务。
5
+ * 懒加载,优先加载项目目录下的模型文件(models/Xenova/...),
6
+ * 不存在则从 HuggingFace 下载。
7
+ *
8
+ * 已下载模型位置:
9
+ * models/Xenova/Qwen2.5-0.5B-Instruct/
10
+ *
11
+ * 环境变量:
12
+ * LOCAL_HYPOTHESIS_MODEL — 模型 ID (默认 Xenova/Qwen2.5-0.5B-Instruct)
13
+ * LOCAL_HYPOTHESIS_DISABLED — 设为 'true' 禁用本地模型
14
+ * MODEL_CACHE_DIR — 缓存目录,缺省自动探测
15
+ */
16
+ export declare class LocalModelService {
17
+ private generator;
18
+ private loadAttempted;
19
+ private modelId;
20
+ constructor(modelId?: string);
21
+ get isEnabled(): boolean;
22
+ get isLoaded(): boolean;
23
+ /**
24
+ * 生成文本 — 兼容 chatJson 接口格式
25
+ */
26
+ generate(userText: string, options?: {
27
+ system?: string;
28
+ temperature?: number;
29
+ maxTokens?: number;
30
+ }): Promise<{
31
+ data?: any;
32
+ error?: string;
33
+ }>;
34
+ /**
35
+ * 尝试从生成文本中提取 JSON
36
+ */
37
+ private tryParse;
38
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalModelService = void 0;
4
+ const path_1 = require("path");
5
+ const fs_1 = require("fs");
6
+ const Logger_1 = require("@akemi-mio/core/logger/Logger");
7
+ /**
8
+ * LocalModelService — 用 transformers.js 在进程内跑小模型
9
+ *
10
+ * 零外部依赖,进程内运行,不需要 Ollama/Python/独立服务。
11
+ * 懒加载,优先加载项目目录下的模型文件(models/Xenova/...),
12
+ * 不存在则从 HuggingFace 下载。
13
+ *
14
+ * 已下载模型位置:
15
+ * models/Xenova/Qwen2.5-0.5B-Instruct/
16
+ *
17
+ * 环境变量:
18
+ * LOCAL_HYPOTHESIS_MODEL — 模型 ID (默认 Xenova/Qwen2.5-0.5B-Instruct)
19
+ * LOCAL_HYPOTHESIS_DISABLED — 设为 'true' 禁用本地模型
20
+ * MODEL_CACHE_DIR — 缓存目录,缺省自动探测
21
+ */
22
+ class LocalModelService {
23
+ generator = null;
24
+ loadAttempted = false;
25
+ modelId;
26
+ constructor(modelId) {
27
+ this.modelId = modelId || process.env.LOCAL_HYPOTHESIS_MODEL || 'Xenova/Qwen2.5-0.5B-Instruct';
28
+ }
29
+ get isEnabled() {
30
+ return process.env.LOCAL_HYPOTHESIS_DISABLED !== 'true';
31
+ }
32
+ get isLoaded() {
33
+ return this.generator !== null;
34
+ }
35
+ /**
36
+ * 生成文本 — 兼容 chatJson 接口格式
37
+ */
38
+ async generate(userText, options) {
39
+ if (!this.isEnabled) {
40
+ return { error: 'local model disabled by env' };
41
+ }
42
+ // 测试环境下跳过实际模型加载
43
+ if (process.env.VITEST || process.env.NODE_ENV === 'test') {
44
+ return { error: 'local model disabled in test environment' };
45
+ }
46
+ // 懒加载模型
47
+ if (!this.generator) {
48
+ if (this.loadAttempted)
49
+ return { error: 'model previously failed to load' };
50
+ this.loadAttempted = true;
51
+ try {
52
+ (0, Logger_1.log)('INFO', 'local_model_loading', { model: this.modelId });
53
+ const { pipeline, env } = await import('@xenova/transformers');
54
+ // 优先使用项目目录下的本地模型
55
+ const modelsDir = await resolveModelsDir();
56
+ const localModelPath = (0, path_1.resolve)(modelsDir, this.modelId);
57
+ const hasLocalFiles = (0, fs_1.existsSync)(localModelPath);
58
+ if (hasLocalFiles) {
59
+ env.localModelPath = modelsDir;
60
+ (0, Logger_1.log)('INFO', 'local_model_use_cache', { path: localModelPath });
61
+ }
62
+ else {
63
+ (0, Logger_1.log)('INFO', 'local_model_no_cache', { path: localModelPath });
64
+ }
65
+ this.generator = await pipeline('text-generation', this.modelId, {
66
+ cache_dir: process.env.MODEL_CACHE_DIR,
67
+ });
68
+ (0, Logger_1.log)('INFO', 'local_model_loaded', { model: this.modelId, local: hasLocalFiles });
69
+ }
70
+ catch (err) {
71
+ (0, Logger_1.log)('WARN', 'local_model_load_failed', { model: this.modelId, error: err.message });
72
+ this.generator = null;
73
+ return { error: `local model load failed: ${err.message}` };
74
+ }
75
+ }
76
+ // 构造 prompts
77
+ const systemMsg = options?.system || '';
78
+ const chatTemplate = systemMsg
79
+ ? `<|im_start|>system\n${systemMsg}<|im_end|>\n<|im_start|>user\n${userText}<|im_end|>\n<|im_start|>assistant\n`
80
+ : `<|im_start|>user\n${userText}<|im_end|>\n<|im_start|>assistant\n`;
81
+ try {
82
+ const result = await this.generator(chatTemplate, {
83
+ max_new_tokens: options?.maxTokens || 768,
84
+ temperature: options?.temperature ?? 0.7,
85
+ do_sample: true,
86
+ });
87
+ const rawText = Array.isArray(result) ? result[0]?.generated_text : result?.generated_text;
88
+ if (!rawText)
89
+ return { error: 'empty generation' };
90
+ // 去掉输入部分,只取生成的文本
91
+ const generated = rawText.slice(chatTemplate.length).trim();
92
+ // 尝试解析 JSON
93
+ const parsed = this.tryParse(generated);
94
+ if (parsed) {
95
+ return { data: parsed };
96
+ }
97
+ return { error: `generated non-JSON: ${generated.slice(0, 100)}` };
98
+ }
99
+ catch (err) {
100
+ (0, Logger_1.log)('WARN', 'local_model_generation_error', { error: err.message });
101
+ return { error: `generation failed: ${err.message}` };
102
+ }
103
+ }
104
+ /**
105
+ * 尝试从生成文本中提取 JSON
106
+ */
107
+ tryParse(text) {
108
+ try {
109
+ return JSON.parse(text);
110
+ }
111
+ catch {
112
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
113
+ if (match) {
114
+ try {
115
+ return JSON.parse(match[1].trim());
116
+ }
117
+ catch {
118
+ // ignore
119
+ }
120
+ }
121
+ const arrMatch = text.match(/\[[\s\S]*\]/);
122
+ if (arrMatch) {
123
+ try {
124
+ return JSON.parse(arrMatch[0]);
125
+ }
126
+ catch {
127
+ // ignore
128
+ }
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ }
134
+ exports.LocalModelService = LocalModelService;
135
+ /** 自动探测项目 models 目录 */
136
+ async function resolveModelsDir() {
137
+ // Electron 环境
138
+ try {
139
+ const { app } = await import('electron');
140
+ // 优先项目目录(dev),回退到 userData/models(安装版)
141
+ if ((0, fs_1.existsSync)((0, path_1.resolve)(app.getAppPath(), 'models'))) {
142
+ return (0, path_1.resolve)(app.getAppPath(), 'models');
143
+ }
144
+ return (0, path_1.resolve)(app.getPath('userData'), 'models');
145
+ }
146
+ catch {
147
+ // not in Electron
148
+ }
149
+ // Node.js 开发环境:逐级向上找 models/
150
+ let dir = process.cwd();
151
+ for (let i = 0; i < 5; i++) {
152
+ const p = (0, path_1.resolve)(dir, 'models');
153
+ if ((0, fs_1.existsSync)(p))
154
+ return p;
155
+ const parent = (0, path_1.resolve)(dir, '@akemi-mio/main');
156
+ if (parent === dir)
157
+ break;
158
+ dir = parent;
159
+ }
160
+ // fallback: CWD/models
161
+ return (0, path_1.resolve)(process.cwd(), 'models');
162
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * NoveltyScorer — 代码层新颖度评估
3
+ *
4
+ * 在 LLM 自评 novelty 之外,通过文本相似度对输入假设进行再评估:
5
+ * 1. 与近期假设(past 30)的 idea 文本对比,Jaccard 相似度 > 0.5 → 扣 15 分
6
+ * 2. 与已拒绝假设的 idea 文本对比,Jaccard 相似度 > 0.6 → 标记为 rejected
7
+ */
8
+ /** Jaccard 相似度:|A ∩ B| / |A ∪ B| */
9
+ export declare function jaccardSimilarity(a: string, b: string): number;
10
+ export interface NoveltyVerdict {
11
+ /** 调整后的新颖度(0-100),可能被扣分 */
12
+ adjustedNovelty: number;
13
+ /** 是否应被自动拒绝(与已拒绝假设高度相似) */
14
+ shouldReject: boolean;
15
+ /** 为什么拒绝 */
16
+ rejectReason?: string;
17
+ /** 与最相似假设的信息 */
18
+ mostSimilarTitle?: string;
19
+ mostSimilarScore: number;
20
+ }
21
+ /**
22
+ * 对一条新假设进行代码层新颖度评估
23
+ *
24
+ * @param candidate 新假设
25
+ * @param recentHypotheses 近期假设(用于扣分)
26
+ * @param rejectedHypotheses 已拒绝假设(用于自动拒绝)
27
+ * @returns NoveltyVerdict
28
+ */
29
+ export declare function evaluateNovelty(candidate: {
30
+ title: string;
31
+ idea: string;
32
+ novelty: number;
33
+ }, recentHypotheses: {
34
+ title: string;
35
+ idea: string;
36
+ novelty: number;
37
+ }[], rejectedHypotheses: {
38
+ title: string;
39
+ idea: string;
40
+ }[]): NoveltyVerdict;