预设装配注入人设与身份宏,并支持编辑采样参数。
让游玩提示词能吃到当前用户角色,同时在设置里可调 generation。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,16 +3,20 @@ import type { PresetPackage } from "../types/preset.js";
|
|||||||
import { isStUnfilledMarker } from "./markers.js";
|
import { isStUnfilledMarker } from "./markers.js";
|
||||||
|
|
||||||
export type MarkerResolver = (identifier: string) => string | null;
|
export type MarkerResolver = (identifier: string) => string | null;
|
||||||
|
export type ContentExpander = (content: string) => string;
|
||||||
|
|
||||||
const defaultMarkerResolver: MarkerResolver = () => null;
|
const defaultMarkerResolver: MarkerResolver = () => null;
|
||||||
|
const defaultContentExpander: ContentExpander = (c) => c;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 prompt_order 装配 preset 消息。
|
* 按 prompt_order 装配 preset 消息。
|
||||||
* 酒馆角色卡 / WI marker 不走 resolver(空洞跳过);自有洞由 resolver 填。
|
* 酒馆角色卡 / WI marker 不走 resolver(空洞跳过);自有洞由 resolver 填。
|
||||||
|
* contentExpander:对最终正文做身份宏展开(@玩家 / {{user}})。
|
||||||
*/
|
*/
|
||||||
export function assemblePresetMessages(
|
export function assemblePresetMessages(
|
||||||
preset: PresetPackage,
|
preset: PresetPackage,
|
||||||
resolveMarker: MarkerResolver = defaultMarkerResolver,
|
resolveMarker: MarkerResolver = defaultMarkerResolver,
|
||||||
|
contentExpander: ContentExpander = defaultContentExpander,
|
||||||
): ChatMessage[] {
|
): ChatMessage[] {
|
||||||
const promptById = new Map(preset.prompts.map((p) => [p.id, p]));
|
const promptById = new Map(preset.prompts.map((p) => [p.id, p]));
|
||||||
const messages: ChatMessage[] = [];
|
const messages: ChatMessage[] = [];
|
||||||
@@ -35,6 +39,7 @@ export function assemblePresetMessages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!content) continue;
|
if (!content) continue;
|
||||||
|
content = contentExpander(content);
|
||||||
|
|
||||||
messages.push({
|
messages.push({
|
||||||
role: entry.role,
|
role: entry.role,
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import type { GenerationParameters, PresetPackage } from "../types/preset.js";
|
||||||
|
import { savePreset } from "./store.js";
|
||||||
|
|
||||||
|
function finiteNumber(value: unknown): number | undefined {
|
||||||
|
if (value === null || value === undefined || value === "") return undefined;
|
||||||
|
const n = typeof value === "number" ? value : Number(value);
|
||||||
|
return Number.isFinite(n) ? n : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalString(value: unknown): string | undefined {
|
||||||
|
if (typeof value !== "string") return undefined;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从表单 / API 体归一化 generation:空字段省略(等于清除)。
|
||||||
|
* 整对象替换,不做与旧值的字段级合并。
|
||||||
|
*/
|
||||||
|
export function normalizeGeneration(
|
||||||
|
input: unknown,
|
||||||
|
): GenerationParameters {
|
||||||
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const raw = input as Record<string, unknown>;
|
||||||
|
const out: GenerationParameters = {};
|
||||||
|
|
||||||
|
const temperature = finiteNumber(raw.temperature);
|
||||||
|
if (temperature !== undefined) out.temperature = temperature;
|
||||||
|
|
||||||
|
const topP = finiteNumber(raw.topP);
|
||||||
|
if (topP !== undefined) out.topP = topP;
|
||||||
|
|
||||||
|
const topK = finiteNumber(raw.topK);
|
||||||
|
if (topK !== undefined) out.topK = topK;
|
||||||
|
|
||||||
|
const minP = finiteNumber(raw.minP);
|
||||||
|
if (minP !== undefined) out.minP = minP;
|
||||||
|
|
||||||
|
const frequencyPenalty = finiteNumber(raw.frequencyPenalty);
|
||||||
|
if (frequencyPenalty !== undefined) out.frequencyPenalty = frequencyPenalty;
|
||||||
|
|
||||||
|
const presencePenalty = finiteNumber(raw.presencePenalty);
|
||||||
|
if (presencePenalty !== undefined) out.presencePenalty = presencePenalty;
|
||||||
|
|
||||||
|
const repetitionPenalty = finiteNumber(raw.repetitionPenalty);
|
||||||
|
if (repetitionPenalty !== undefined) {
|
||||||
|
out.repetitionPenalty = repetitionPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxContextTokens = finiteNumber(raw.maxContextTokens);
|
||||||
|
if (maxContextTokens !== undefined) out.maxContextTokens = maxContextTokens;
|
||||||
|
|
||||||
|
const maxOutputTokens = finiteNumber(raw.maxOutputTokens);
|
||||||
|
if (maxOutputTokens !== undefined) out.maxOutputTokens = maxOutputTokens;
|
||||||
|
|
||||||
|
if (typeof raw.stream === "boolean") out.stream = raw.stream;
|
||||||
|
|
||||||
|
const reasoningEffort = optionalString(raw.reasoningEffort);
|
||||||
|
if (reasoningEffort !== undefined) out.reasoningEffort = reasoningEffort;
|
||||||
|
|
||||||
|
const verbosity = optionalString(raw.verbosity);
|
||||||
|
if (verbosity !== undefined) out.verbosity = verbosity;
|
||||||
|
|
||||||
|
const seed = finiteNumber(raw.seed);
|
||||||
|
if (seed !== undefined) out.seed = seed;
|
||||||
|
|
||||||
|
const variants = finiteNumber(raw.variants);
|
||||||
|
if (variants !== undefined) out.variants = variants;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyPresetGeneration(
|
||||||
|
preset: PresetPackage,
|
||||||
|
generation: unknown,
|
||||||
|
): PresetPackage {
|
||||||
|
return {
|
||||||
|
...preset,
|
||||||
|
generation: normalizeGeneration(generation),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchPresetGeneration(
|
||||||
|
preset: PresetPackage,
|
||||||
|
generation: unknown,
|
||||||
|
): PresetPackage {
|
||||||
|
return savePreset(applyPresetGeneration(preset, generation));
|
||||||
|
}
|
||||||
@@ -5,10 +5,9 @@ import type {
|
|||||||
PresetPromptRole,
|
PresetPromptRole,
|
||||||
} from "../types/preset.js";
|
} from "../types/preset.js";
|
||||||
|
|
||||||
/** 酒馆角色卡 / WI marker:导入后保留位置,运行时不填我们的正文。 */
|
/** 酒馆角色卡 / WI marker:导入后保留位置;personaDescription 由当前用户角色填。 */
|
||||||
export const ST_UNFILLED_MARKERS = new Set([
|
export const ST_UNFILLED_MARKERS = new Set([
|
||||||
"worldInfoBefore",
|
"worldInfoBefore",
|
||||||
"personaDescription",
|
|
||||||
"charDescription",
|
"charDescription",
|
||||||
"charPersonality",
|
"charPersonality",
|
||||||
"scenario",
|
"scenario",
|
||||||
@@ -16,6 +15,8 @@ export const ST_UNFILLED_MARKERS = new Set([
|
|||||||
"dialogueExamples",
|
"dialogueExamples",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const PERSONA_DESCRIPTION_MARKER = "personaDescription";
|
||||||
|
|
||||||
export const CHAT_HISTORY_MARKER = "chatHistory";
|
export const CHAT_HISTORY_MARKER = "chatHistory";
|
||||||
export const WORLD_BOOK_BEFORE = "worldBookBefore";
|
export const WORLD_BOOK_BEFORE = "worldBookBefore";
|
||||||
export const WORLD_BOOK_AFTER = "worldBookAfter";
|
export const WORLD_BOOK_AFTER = "worldBookAfter";
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import { expandIdentityDirectives } from "../directives/expand.js";
|
||||||
import type { ChatMessage } from "../llm/client.js";
|
import type { ChatMessage } from "../llm/client.js";
|
||||||
|
import {
|
||||||
|
getActivePersona,
|
||||||
|
type PersonaDirective,
|
||||||
|
} from "../persona/store.js";
|
||||||
import type { PresetPackage } from "../types/preset.js";
|
import type { PresetPackage } from "../types/preset.js";
|
||||||
import { assemblePresetMessages } from "./assembler.js";
|
import { assemblePresetMessages } from "./assembler.js";
|
||||||
import { applyExtendedMarkers } from "./markers.js";
|
import {
|
||||||
|
applyExtendedMarkers,
|
||||||
|
PERSONA_DESCRIPTION_MARKER,
|
||||||
|
} from "./markers.js";
|
||||||
import {
|
import {
|
||||||
resolveWorldInfoMarker,
|
resolveWorldInfoMarker,
|
||||||
type WorldInfoEntry,
|
type WorldInfoEntry,
|
||||||
@@ -35,11 +43,28 @@ export function assemblePlayWorkerMessages(params: {
|
|||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
preset: PresetPackage;
|
preset: PresetPackage;
|
||||||
pack: WorldInfoPack;
|
pack: WorldInfoPack;
|
||||||
|
/** 缺省用当前选用(试跑 / 未绑会话)。创作期应传入创作默认人设。 */
|
||||||
|
persona?: PersonaDirective | null;
|
||||||
}): ChatMessage[] {
|
}): ChatMessage[] {
|
||||||
const framed = applyExtendedMarkers(params.preset);
|
const framed = applyExtendedMarkers(params.preset);
|
||||||
const pack = withTaskSystem(params.pack, params.systemPrompt);
|
const pack = withTaskSystem(params.pack, params.systemPrompt);
|
||||||
return assemblePresetMessages(framed, (id) =>
|
const persona =
|
||||||
resolveWorldInfoMarker(pack, id),
|
params.persona !== undefined ? params.persona : getActivePersona();
|
||||||
|
const dirCtx = persona
|
||||||
|
? { persona: { name: persona.name, description: persona.description } }
|
||||||
|
: {};
|
||||||
|
return assemblePresetMessages(
|
||||||
|
framed,
|
||||||
|
(id) => {
|
||||||
|
if (id === PERSONA_DESCRIPTION_MARKER) {
|
||||||
|
const desc = persona?.description?.trim();
|
||||||
|
if (desc) return desc;
|
||||||
|
const name = persona?.name?.trim();
|
||||||
|
return name ? `姓名:${name}` : null;
|
||||||
|
}
|
||||||
|
return resolveWorldInfoMarker(pack, id);
|
||||||
|
},
|
||||||
|
(content) => expandIdentityDirectives(content, dirCtx).text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +97,7 @@ function formatHistoryMessages(msgs: ChatMessage[]): string {
|
|||||||
export function frameMessagesWithPreset(
|
export function frameMessagesWithPreset(
|
||||||
preset: PresetPackage,
|
preset: PresetPackage,
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
|
persona?: PersonaDirective | null,
|
||||||
): ChatMessage[] {
|
): ChatMessage[] {
|
||||||
let splitAt = messages.length;
|
let splitAt = messages.length;
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
@@ -112,6 +138,7 @@ export function frameMessagesWithPreset(
|
|||||||
turn,
|
turn,
|
||||||
postTurn: [],
|
postTurn: [],
|
||||||
},
|
},
|
||||||
|
persona,
|
||||||
});
|
});
|
||||||
|
|
||||||
return continuation.length > 0 ? [...framed, ...continuation] : framed;
|
return continuation.length > 0 ? [...framed, ...continuation] : framed;
|
||||||
|
|||||||
+4
-4
@@ -5,12 +5,12 @@ import type { WorldInfoPack } from "./world-info-pack.js";
|
|||||||
|
|
||||||
/** 试跑窗口用的短契约,不是 play worker SKILL。 */
|
/** 试跑窗口用的短契约,不是 play worker SKILL。 */
|
||||||
export const PROBE_SYSTEM_PROMPT =
|
export const PROBE_SYSTEM_PROMPT =
|
||||||
"试跑本条预设:根据已拼好的上下文写一轮短回复。产物是对白或叙述,对应本轮用户输入。";
|
"试跑本条预设:对本轮输入做忠实扩写。不增删情节,不另起冲突。";
|
||||||
|
|
||||||
export const DEFAULT_PROBE_CONTEXT = {
|
export const DEFAULT_PROBE_CONTEXT = {
|
||||||
loreBefore: "一座雨夜的港口旅馆。柜台点着油灯。",
|
loreBefore: "写作要求:对本轮输入做忠实扩写;只展开已给出的动作与信息,不另起情节。",
|
||||||
history: "店员:今晚只剩阁楼那间。\n你:好。",
|
history: "扩写范围仅限本轮输入,勿补前因后果或无关对话。",
|
||||||
loreAfter: "时间:深夜。地点:旅馆大厅。",
|
loreAfter: "",
|
||||||
postTurn: "",
|
postTurn: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { toActiveSkillSnapshot } from "../skills/snapshot.js";
|
|||||||
import { runWorkerSkill } from "../worker/executor.js";
|
import { runWorkerSkill } from "../worker/executor.js";
|
||||||
import { resolveWorkerId } from "../worker/resolve-id.js";
|
import { resolveWorkerId } from "../worker/resolve-id.js";
|
||||||
import { resolveWorkerLlmProvider } from "../skills/worker-llm.js";
|
import { resolveWorkerLlmProvider } from "../skills/worker-llm.js";
|
||||||
|
import { personaForLifecycle } from "../persona/store.js";
|
||||||
import {
|
import {
|
||||||
DIALOGUE_HISTORY_TAG,
|
DIALOGUE_HISTORY_TAG,
|
||||||
appendDialogueHistoryTurn,
|
appendDialogueHistoryTurn,
|
||||||
@@ -2272,6 +2273,8 @@ export class PhaseRuntime {
|
|||||||
bindings: (await loadSkill(activeSkill.name)).workerLlmBindings,
|
bindings: (await loadSkill(activeSkill.name)).workerLlmBindings,
|
||||||
slots,
|
slots,
|
||||||
fallbackLlm: this.llm,
|
fallbackLlm: this.llm,
|
||||||
|
getPersona: () =>
|
||||||
|
personaForLifecycle(inferLifecycleStage(this.session)),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onMessage(
|
this.onMessage(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
wrapLlmForSession,
|
wrapLlmForSession,
|
||||||
type LlmTrackingRef,
|
type LlmTrackingRef,
|
||||||
} from "../llm/preset-wrapper.js";
|
} from "../llm/preset-wrapper.js";
|
||||||
|
import type { PersonaDirective } from "../persona/store.js";
|
||||||
import type { ParsedWorkerSkill, SkillWorkerLlmBindings } from "./types.js";
|
import type { ParsedWorkerSkill, SkillWorkerLlmBindings } from "./types.js";
|
||||||
|
|
||||||
export type { LlmTrackingContext } from "../llm/token-tracker.js";
|
export type { LlmTrackingContext } from "../llm/token-tracker.js";
|
||||||
@@ -16,6 +17,7 @@ export function createLlmForProfileId(
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
fallback: LlmProvider,
|
fallback: LlmProvider,
|
||||||
trackingRef?: LlmTrackingRef,
|
trackingRef?: LlmTrackingRef,
|
||||||
|
getPersona?: () => PersonaDirective | null,
|
||||||
): LlmProvider {
|
): LlmProvider {
|
||||||
const profile = getApiProfile(profileId);
|
const profile = getApiProfile(profileId);
|
||||||
if (!profile?.apiKey?.trim()) {
|
if (!profile?.apiKey?.trim()) {
|
||||||
@@ -25,6 +27,8 @@ export function createLlmForProfileId(
|
|||||||
return wrapLlmForSession(
|
return wrapLlmForSession(
|
||||||
new OpenAiCompatibleProvider(profileToLlmConfig(profile)),
|
new OpenAiCompatibleProvider(profileToLlmConfig(profile)),
|
||||||
trackingRef,
|
trackingRef,
|
||||||
|
undefined,
|
||||||
|
getPersona,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,14 +47,17 @@ export function resolveWorkerLlmProvider(options: {
|
|||||||
slots: Record<string, unknown>;
|
slots: Record<string, unknown>;
|
||||||
fallbackLlm: LlmProvider;
|
fallbackLlm: LlmProvider;
|
||||||
trackingRef?: LlmTrackingRef;
|
trackingRef?: LlmTrackingRef;
|
||||||
|
getPersona?: () => PersonaDirective | null;
|
||||||
}): LlmProvider {
|
}): LlmProvider {
|
||||||
const { worker, bindings, slots, fallbackLlm, trackingRef } = options;
|
const { worker, bindings, slots, fallbackLlm, trackingRef, getPersona } =
|
||||||
|
options;
|
||||||
|
|
||||||
if (worker.llmProfileId?.trim()) {
|
if (worker.llmProfileId?.trim()) {
|
||||||
return createLlmForProfileId(
|
return createLlmForProfileId(
|
||||||
worker.llmProfileId.trim(),
|
worker.llmProfileId.trim(),
|
||||||
fallbackLlm,
|
fallbackLlm,
|
||||||
trackingRef,
|
trackingRef,
|
||||||
|
getPersona,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +69,7 @@ export function resolveWorkerLlmProvider(options: {
|
|||||||
workerBinding.byRole[roleId].trim(),
|
workerBinding.byRole[roleId].trim(),
|
||||||
fallbackLlm,
|
fallbackLlm,
|
||||||
trackingRef,
|
trackingRef,
|
||||||
|
getPersona,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +79,7 @@ export function resolveWorkerLlmProvider(options: {
|
|||||||
workerBinding.profileId.trim(),
|
workerBinding.profileId.trim(),
|
||||||
fallbackLlm,
|
fallbackLlm,
|
||||||
trackingRef,
|
trackingRef,
|
||||||
|
getPersona,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +88,7 @@ export function resolveWorkerLlmProvider(options: {
|
|||||||
bindings.defaultProfileId.trim(),
|
bindings.defaultProfileId.trim(),
|
||||||
fallbackLlm,
|
fallbackLlm,
|
||||||
trackingRef,
|
trackingRef,
|
||||||
|
getPersona,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ describe("assemblePlayWorkerMessages", () => {
|
|||||||
systemPrompt: "SKILL",
|
systemPrompt: "SKILL",
|
||||||
preset: report.preset,
|
preset: report.preset,
|
||||||
pack,
|
pack,
|
||||||
|
persona: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(messages.map((m) => [m.role, m.content])).toEqual([
|
expect(messages.map((m) => [m.role, m.content])).toEqual([
|
||||||
@@ -107,6 +108,7 @@ describe("assemblePlayWorkerMessages", () => {
|
|||||||
turn: "本轮",
|
turn: "本轮",
|
||||||
postTurn: [],
|
postTurn: [],
|
||||||
},
|
},
|
||||||
|
persona: null,
|
||||||
});
|
});
|
||||||
expect(messages[0]).toEqual({ role: "system", content: "<世界书>" });
|
expect(messages[0]).toEqual({ role: "system", content: "<世界书>" });
|
||||||
expect(messages.some((m) => m.content === "创作步骤契约")).toBe(false);
|
expect(messages.some((m) => m.content === "创作步骤契约")).toBe(false);
|
||||||
@@ -148,4 +150,53 @@ describe("assemblePlayWorkerMessages", () => {
|
|||||||
});
|
});
|
||||||
expect(messages.map((m) => m.content)).toEqual(["HIST"]);
|
expect(messages.map((m) => m.content)).toEqual(["HIST"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fills personaDescription and expands @玩家 with the given persona", () => {
|
||||||
|
const report = importSillyTavernPreset({
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
identifier: "personaDescription",
|
||||||
|
name: "Persona",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: "main",
|
||||||
|
name: "Main",
|
||||||
|
role: "system",
|
||||||
|
content: "对 @玩家 说话",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: "chatHistory",
|
||||||
|
name: "History",
|
||||||
|
role: "system",
|
||||||
|
marker: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt_order: [
|
||||||
|
{
|
||||||
|
character_id: 1,
|
||||||
|
order: [
|
||||||
|
{ identifier: "personaDescription", enabled: true },
|
||||||
|
{ identifier: "main", enabled: true },
|
||||||
|
{ identifier: "chatHistory", enabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const messages = assemblePlayWorkerMessages({
|
||||||
|
systemPrompt: "SKILL",
|
||||||
|
preset: report.preset,
|
||||||
|
pack: {
|
||||||
|
worldBookBefore: [],
|
||||||
|
history: "昨日",
|
||||||
|
worldBookAfter: [],
|
||||||
|
turn: "本轮",
|
||||||
|
postTurn: [],
|
||||||
|
},
|
||||||
|
persona: { name: "@玩家", description: "占位人设" },
|
||||||
|
});
|
||||||
|
expect(messages.some((m) => m.content === "占位人设")).toBe(true);
|
||||||
|
expect(messages.some((m) => m.content === "对 @玩家 说话")).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ describe("assemblePresetProbe", () => {
|
|||||||
`### 任务契约\n\n${PROBE_SYSTEM_PROMPT}\n\n### 试跑设定\n\n${DEFAULT_PROBE_CONTEXT.loreBefore}`,
|
`### 任务契约\n\n${PROBE_SYSTEM_PROMPT}\n\n### 试跑设定\n\n${DEFAULT_PROBE_CONTEXT.loreBefore}`,
|
||||||
],
|
],
|
||||||
["system", DEFAULT_PROBE_CONTEXT.history],
|
["system", DEFAULT_PROBE_CONTEXT.history],
|
||||||
["system", `### 本轮状态\n\n${DEFAULT_PROBE_CONTEXT.loreAfter}`],
|
|
||||||
["user", "我推开门。"],
|
["user", "我推开门。"],
|
||||||
["system", "PHI"],
|
["system", "PHI"],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
listAllPresetEntries,
|
listAllPresetEntries,
|
||||||
listEnabledPresetEntries,
|
listEnabledPresetEntries,
|
||||||
} from "../src/preset/entries.js";
|
} from "../src/preset/entries.js";
|
||||||
|
import {
|
||||||
|
applyPresetGeneration,
|
||||||
|
normalizeGeneration,
|
||||||
|
} from "../src/preset/generation.js";
|
||||||
import { importSillyTavernPreset } from "../src/preset/importer.js";
|
import { importSillyTavernPreset } from "../src/preset/importer.js";
|
||||||
|
|
||||||
const samplePreset = {
|
const samplePreset = {
|
||||||
@@ -170,3 +174,35 @@ describe("applyPresetEntryPatches", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("normalizeGeneration / applyPresetGeneration", () => {
|
||||||
|
it("keeps finite numbers and clears empty fields on replace", () => {
|
||||||
|
expect(
|
||||||
|
normalizeGeneration({
|
||||||
|
temperature: "0.9",
|
||||||
|
topP: "",
|
||||||
|
maxOutputTokens: 4096,
|
||||||
|
reasoningEffort: " high ",
|
||||||
|
verbosity: " ",
|
||||||
|
stream: true,
|
||||||
|
seed: "not-a-number",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
temperature: 0.9,
|
||||||
|
maxOutputTokens: 4096,
|
||||||
|
reasoningEffort: "high",
|
||||||
|
stream: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces preset generation wholesale", () => {
|
||||||
|
const report = importSillyTavernPreset(samplePreset);
|
||||||
|
expect(report.preset.generation.temperature).toBe(1);
|
||||||
|
const next = applyPresetGeneration(report.preset, {
|
||||||
|
topP: 0.8,
|
||||||
|
maxOutputTokens: 2048,
|
||||||
|
});
|
||||||
|
expect(next.generation).toEqual({ topP: 0.8, maxOutputTokens: 2048 });
|
||||||
|
expect(next.prompts).toBe(report.preset.prompts);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user