新增转述回合引擎,用插入工具把对话落成带相对序的产物。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
import type { ChatMessage } from "../llm/client.js";
|
||||||
|
import {
|
||||||
|
effectiveDictateOrder,
|
||||||
|
sortDictateProducts,
|
||||||
|
type DictateChatTurn,
|
||||||
|
type DictateProduct,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼给模型的上下文:系统提示 + 全部产物(按相对序)+ 全量对话。
|
||||||
|
* 清空对话后,只剩产物块,产物即浓缩上下文。
|
||||||
|
*/
|
||||||
|
export function buildDictateMessages(params: {
|
||||||
|
systemPrompt: string;
|
||||||
|
products: DictateProduct[];
|
||||||
|
dialogue: DictateChatTurn[];
|
||||||
|
}): ChatMessage[] {
|
||||||
|
const parts: string[] = [];
|
||||||
|
const products = sortDictateProducts(params.products);
|
||||||
|
|
||||||
|
if (products.length > 0) {
|
||||||
|
parts.push("## 当前产物(已写入黑板,按相对序;对话清空后仍保留)");
|
||||||
|
for (const p of products) {
|
||||||
|
const ord = effectiveDictateOrder(p);
|
||||||
|
parts.push(`### ${p.tag} 〔order=${ord}〕\n${p.content}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts.push("## 当前产物\n(尚无)");
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push("## 对话(全量)");
|
||||||
|
if (params.dialogue.length === 0) {
|
||||||
|
parts.push("(尚无对话;等用户开口)");
|
||||||
|
} else {
|
||||||
|
for (const turn of params.dialogue) {
|
||||||
|
const who = turn.role === "user" ? "用户" : "助手";
|
||||||
|
parts.push(`【${who}】\n${turn.text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ role: "system", content: params.systemPrompt },
|
||||||
|
{ role: "user", content: parts.join("\n\n") },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从会话消息里抽出转述式对话(用户输入 + 助手回复) */
|
||||||
|
export function extractDictateDialogue(
|
||||||
|
messages: Array<{ role?: string; kind?: string; text?: string; body?: string }>,
|
||||||
|
): DictateChatTurn[] {
|
||||||
|
const out: DictateChatTurn[] = [];
|
||||||
|
for (const m of messages) {
|
||||||
|
const text = (m.body ?? m.text ?? "").trim();
|
||||||
|
if (!text) continue;
|
||||||
|
if (m.role === "user" || m.kind === "user_input") {
|
||||||
|
out.push({ role: "user", text });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (m.kind === "dictate_reply") {
|
||||||
|
out.push({ role: "assistant", text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export { buildDictateSystemPrompt, DICTATE_SYSTEM_PROMPT } from "./prompt.js";
|
||||||
|
export {
|
||||||
|
CREATION_INTAKE_MODE_TAG,
|
||||||
|
DICTATE_MODE_VALUE,
|
||||||
|
DICTATE_ORDER_META_KEY,
|
||||||
|
isDictateModeValue,
|
||||||
|
isAllowedProductTag,
|
||||||
|
defaultDictateOrder,
|
||||||
|
effectiveDictateOrder,
|
||||||
|
compareDictateProducts,
|
||||||
|
sortDictateProducts,
|
||||||
|
parseDictateOrder,
|
||||||
|
type DictateProduct,
|
||||||
|
type DictateChatTurn,
|
||||||
|
} from "./types.js";
|
||||||
|
export {
|
||||||
|
buildDictateMessages,
|
||||||
|
extractDictateDialogue,
|
||||||
|
} from "./context.js";
|
||||||
|
export { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
|
||||||
|
export { runDictateTurn, type DictateTurnResult } from "./turn.js";
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* 转述整理式 · 创作进料专用系统提示
|
||||||
|
*
|
||||||
|
* 拼装:本提示(system)+ 当前产物(按相对序)+ 全量对话。
|
||||||
|
* 仅用于创作层;开玩后由游玩管线接棒。
|
||||||
|
*
|
||||||
|
* 产物只许用 insert toolcall;聊天只做确认与建议。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function buildDictateSystemPrompt(opts?: {
|
||||||
|
recipeName?: string;
|
||||||
|
recipeBrief?: string;
|
||||||
|
}): string {
|
||||||
|
const recipeBlock =
|
||||||
|
opts?.recipeName || opts?.recipeBrief
|
||||||
|
? `
|
||||||
|
## 本局配方
|
||||||
|
名称:${opts.recipeName ?? "(未命名)"}
|
||||||
|
${opts.recipeBrief ? `要点:${opts.recipeBrief}` : ""}
|
||||||
|
按上述适用与原则整理产物;材料只够做本配方时,就只做本配方范围内的事。
|
||||||
|
`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `你是转述整理助手:在**创作进料**阶段,把用户原话整理成黑板产物。作用对象是本局创作上下文(对话可清空,产物保留)。
|
||||||
|
|
||||||
|
**本提示只用于创作。** 开玩后的叙事由程序另调游玩 worker;不要假装在跑游玩回合。
|
||||||
|
|
||||||
|
## 落盘(仅允许 toolcall)
|
||||||
|
产物**只能**用工具 **insert** 写入,禁止把设定/格式/开场全文只写在聊天里冒充已落盘。
|
||||||
|
- \`position\`:插入位置(tag),如 \`用户.需求\`、\`设计.正文组成\`、\`设计.开场白\`
|
||||||
|
- \`content\`:该位置正文
|
||||||
|
- \`order\`(可选):相对顺序,数字可负、可为 0、可正;**越小越靠前**
|
||||||
|
|
||||||
|
**order 怎么取:**
|
||||||
|
1. 变化越频繁 → 数字越大(越靠后)。例:开场白常改 > 正文组成 > 需求/美学。
|
||||||
|
2. 改动频率差不多 → 越重要数字越小(越靠前)。例:用户.需求 通常最前。
|
||||||
|
3. 同轮可多次 insert;程序按 order 升序拼进上下文。省略 order 时沿用该 position 原序,或按 tag 默认。
|
||||||
|
|
||||||
|
**自然语言回复**只写:短确认、是否这个、建议补充 A/B/C、提问。不要复述 insert 全文。
|
||||||
|
|
||||||
|
**分清两件事:**
|
||||||
|
- **正文组成(格式)**:insert → \`设计.正文组成\`(壳与分区,非故事)。
|
||||||
|
- **开场白(内容)**:insert → \`设计.开场白\`(按格式写的首屏);落档/开玩同步 \`输出.开场白\`。
|
||||||
|
${recipeBlock}
|
||||||
|
## 按用户本轮意图择一为主
|
||||||
|
### A · 钉设定
|
||||||
|
1. insert 设定类 position(保留原意原词);order 偏小(靠前)。
|
||||||
|
2. 回复:确认体验(2~4 句 + 问是否这个)+ 建议补充(1~2 缺口,各 A/B/C)。
|
||||||
|
|
||||||
|
### B · 钉正文格式
|
||||||
|
1. 必须 insert \`设计.正文组成\`(见规格);order 居中(约 0)。
|
||||||
|
2. 回复短说明已钉壳与主块。
|
||||||
|
|
||||||
|
### C · 生成开场白
|
||||||
|
1. 先有 \`设计.正文组成\`(否则先 B;缺设定先 A)。
|
||||||
|
2. insert \`设计.开场白\`:Markdown 主阅读;岔路用 \`## 岔路\`;勿写 【body】/【footer】。order 偏大(靠后)。
|
||||||
|
3. 回复只短说已写入;全文在 content。
|
||||||
|
|
||||||
|
## 常用 position 与默认序感
|
||||||
|
- 用户.需求、设计.美学纲领等设定:靠前(负)
|
||||||
|
- 设计.正文组成:中间(0 附近)
|
||||||
|
- 设计.开场白:靠后(正)
|
||||||
|
|
||||||
|
## 设计.正文组成 · 规格(格式)
|
||||||
|
JSON 即可。核心:\`正文.呈现壳.shell_id\`、\`正文.可见块[]\`(区域 body/footer 等)。
|
||||||
|
文本局默认 shell_id=\`prose\`;至少一块 body。要选项再声明 footer。
|
||||||
|
|
||||||
|
## 设计.开场白 · 规格(内容)
|
||||||
|
按正文组成职责写满主阅读;Markdown;确认与建议不进 content。
|
||||||
|
|
||||||
|
## 其它
|
||||||
|
对话明显过长时 clear_dialogue(产物保留);否则不要清。
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated 用 buildDictateSystemPrompt() */
|
||||||
|
export const DICTATE_SYSTEM_PROMPT = buildDictateSystemPrompt();
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { ToolDefinition } from "../llm/client.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转述创作期工具:只允许用 toolcall 写产物。
|
||||||
|
* insert(position, content, order?) —— 打标记落盘并带相对序。
|
||||||
|
*/
|
||||||
|
export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "insert",
|
||||||
|
description:
|
||||||
|
"唯一写产物入口:把正文插入黑板。确认/建议/思考只写在普通回复,禁止放进 content。用 order 标相对先后以便拼装。",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
position: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"插入位置(产物 tag)。须以「用户.」或「设计.」开头。例:用户.需求、设计.正文组成、设计.开场白",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"落入该位置的正文。设计.正文组成=格式 JSON;设计.开场白=可读开场(Markdown,用 ## 分段,勿写【body】、勿夹确认/建议)",
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: "number",
|
||||||
|
description:
|
||||||
|
"相对顺序(可负、0、正)。越小越靠前。原则:越常改越大(靠后);改动频率差不多时,越重要越小(靠前)。省略则沿用原序或按 tag 默认。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["position", "content"],
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "clear_dialogue",
|
||||||
|
description:
|
||||||
|
"对话过长时清空对话记录。产物保留。清空后继续用自然语言回复用户。",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
reason: {
|
||||||
|
type: "string",
|
||||||
|
description: "为何清空(一句话)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["reason"],
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import type { LlmProvider } from "../llm/client.js";
|
||||||
|
import {
|
||||||
|
createLocalLlmDriver,
|
||||||
|
type AgentDriver,
|
||||||
|
} from "../runtime/driver.js";
|
||||||
|
import { buildDictateMessages } from "./context.js";
|
||||||
|
import { buildDictateSystemPrompt } from "./prompt.js";
|
||||||
|
import { DICTATE_TOOL_DEFINITIONS } from "./tools.js";
|
||||||
|
import {
|
||||||
|
isAllowedProductTag,
|
||||||
|
parseDictateOrder,
|
||||||
|
type DictateChatTurn,
|
||||||
|
type DictateProduct,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
export type DictateTurnHandlers = {
|
||||||
|
listProducts: () => DictateProduct[];
|
||||||
|
/** order 省略时由实现沿用原序或默认 */
|
||||||
|
writeProduct: (tag: string, content: string, order?: number) => void;
|
||||||
|
clearDialogue: () => void;
|
||||||
|
onToolCall?: (name: string, detail: string) => void;
|
||||||
|
onThinkingDelta?: (delta: string) => void;
|
||||||
|
onThinkingDone?: (text: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DictateTurnResult = {
|
||||||
|
reply: string;
|
||||||
|
iterations: number;
|
||||||
|
wroteTags: string[];
|
||||||
|
clearedDialogue: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跑一轮转述整理:上下文 = 预设提示 + 按相对序的产物 + 全量对话。
|
||||||
|
* 产物只许 insert(position, content, order?);最终文本即对用户回复。
|
||||||
|
*/
|
||||||
|
export async function runDictateTurn(params: {
|
||||||
|
llm: LlmProvider;
|
||||||
|
dialogue: DictateChatTurn[];
|
||||||
|
handlers: DictateTurnHandlers;
|
||||||
|
systemPrompt?: string;
|
||||||
|
recipeName?: string;
|
||||||
|
recipeBrief?: string;
|
||||||
|
driver?: AgentDriver;
|
||||||
|
}): Promise<DictateTurnResult> {
|
||||||
|
const driver = params.driver ?? createLocalLlmDriver(params.llm);
|
||||||
|
const wroteTags: string[] = [];
|
||||||
|
let clearedDialogue = false;
|
||||||
|
let dialogue = [...params.dialogue];
|
||||||
|
|
||||||
|
const systemPrompt =
|
||||||
|
params.systemPrompt ??
|
||||||
|
buildDictateSystemPrompt({
|
||||||
|
recipeName: params.recipeName,
|
||||||
|
recipeBrief: params.recipeBrief,
|
||||||
|
});
|
||||||
|
const assembled = buildDictateMessages({
|
||||||
|
systemPrompt,
|
||||||
|
products: params.handlers.listProducts(),
|
||||||
|
dialogue,
|
||||||
|
});
|
||||||
|
// driver 会再前置 system;此处只传 user 拼装块,避免双重 system
|
||||||
|
const userBlock = assembled.filter((m) => m.role !== "system");
|
||||||
|
|
||||||
|
const run = await driver.run({
|
||||||
|
system: systemPrompt,
|
||||||
|
messages: userBlock,
|
||||||
|
tools: DICTATE_TOOL_DEFINITIONS,
|
||||||
|
caller: "dictate_agent",
|
||||||
|
label: "转述整理",
|
||||||
|
onThinkingDelta: params.handlers.onThinkingDelta,
|
||||||
|
onThinkingDone: params.handlers.onThinkingDone,
|
||||||
|
handleStep: (calls) => {
|
||||||
|
const results = calls.map((call) => {
|
||||||
|
const args = safeParseArgs(call.arguments);
|
||||||
|
if (call.name === "insert" || call.name === "write_product") {
|
||||||
|
const tag = String(
|
||||||
|
args.position ?? args.tag ?? "",
|
||||||
|
).trim();
|
||||||
|
const content = String(args.content ?? "");
|
||||||
|
if (!isAllowedProductTag(tag)) {
|
||||||
|
const err = `拒绝写入:position 必须以「用户.」或「设计.」开头(收到:${tag || "空"})`;
|
||||||
|
params.handlers.onToolCall?.(call.name, err);
|
||||||
|
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||||
|
}
|
||||||
|
if (!content.trim()) {
|
||||||
|
const err = "content 不能为空";
|
||||||
|
params.handlers.onToolCall?.(call.name, err);
|
||||||
|
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||||
|
}
|
||||||
|
const order = parseDictateOrder(args.order);
|
||||||
|
params.handlers.writeProduct(tag, content, order);
|
||||||
|
wroteTags.push(tag);
|
||||||
|
const detail =
|
||||||
|
order === undefined ? tag : `${tag} order=${order}`;
|
||||||
|
params.handlers.onToolCall?.(call.name, detail);
|
||||||
|
return {
|
||||||
|
callId: call.id,
|
||||||
|
content: JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
position: tag,
|
||||||
|
...(order !== undefined ? { order } : {}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (call.name === "clear_dialogue") {
|
||||||
|
const reason = String(args.reason ?? "").trim() || "上下文过长";
|
||||||
|
params.handlers.clearDialogue();
|
||||||
|
clearedDialogue = true;
|
||||||
|
dialogue = [];
|
||||||
|
params.handlers.onToolCall?.(call.name, reason);
|
||||||
|
return {
|
||||||
|
callId: call.id,
|
||||||
|
content: JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
cleared: true,
|
||||||
|
note: "对话已清空;产物仍在。后续上下文以产物为准。",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const err = `未知工具:${call.name}`;
|
||||||
|
params.handlers.onToolCall?.(call.name, err);
|
||||||
|
return { callId: call.id, content: JSON.stringify({ error: err }) };
|
||||||
|
});
|
||||||
|
return { kind: "continue" as const, results };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// driver 的 messages 在首轮已固定;clear 后若还要续写依赖模型已知工具结果。
|
||||||
|
// 若模型只调工具不说话,给一个兜底回复。
|
||||||
|
let reply = "";
|
||||||
|
if (run.stop.kind === "text") {
|
||||||
|
reply = run.stop.content.trim();
|
||||||
|
}
|
||||||
|
if (!reply) {
|
||||||
|
if (wroteTags.length) {
|
||||||
|
reply = `已写入:${[...new Set(wroteTags)].join("、")}。还有要补充的吗?`;
|
||||||
|
} else if (clearedDialogue) {
|
||||||
|
reply = "对话已清空,产物都还在。可以继续说。";
|
||||||
|
} else {
|
||||||
|
reply = "我在听,请继续说你想要的体验或设定。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reply,
|
||||||
|
iterations: run.iterations,
|
||||||
|
wroteTags: [...new Set(wroteTags)],
|
||||||
|
clearedDialogue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeParseArgs(raw: string): Record<string, unknown> {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw || "{}");
|
||||||
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||||
|
return parsed as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/** 转述整理式 · 类型与常量 */
|
||||||
|
|
||||||
|
export const CREATION_INTAKE_MODE_TAG = "创作.进料模式";
|
||||||
|
export const DICTATE_MODE_VALUE = "dictate";
|
||||||
|
|
||||||
|
/** 产物相对序写在黑板 item.metadata 里 */
|
||||||
|
export const DICTATE_ORDER_META_KEY = "dictateOrder";
|
||||||
|
|
||||||
|
export type DictateProduct = {
|
||||||
|
tag: string;
|
||||||
|
content: string;
|
||||||
|
/** 相对顺序:负数靠前,正数靠后;缺省时按 tag 启发式 */
|
||||||
|
order?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DictateChatTurn = {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isDictateModeValue(raw: string | undefined | null): boolean {
|
||||||
|
return (raw ?? "").trim() === DICTATE_MODE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 允许写入的产物 tag */
|
||||||
|
export function isAllowedProductTag(tag: string): boolean {
|
||||||
|
const t = tag.trim();
|
||||||
|
if (!t) return false;
|
||||||
|
if (t.includes("\n") || t.includes("\0")) return false;
|
||||||
|
return t.startsWith("用户.") || t.startsWith("设计.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未显式给 order 时的默认相对序。
|
||||||
|
* 越稳定/重要越靠前(更负);越常改越靠后(更正)。
|
||||||
|
*/
|
||||||
|
export function defaultDictateOrder(tag: string): number {
|
||||||
|
const t = tag.trim();
|
||||||
|
if (t === "用户.需求") return -40;
|
||||||
|
if (/美学|纲领|禁忌|示例/.test(t)) return -30;
|
||||||
|
if (/文风|叙事指南|篇幅|结构/.test(t)) return -20;
|
||||||
|
if (/正文组成|回复格式/.test(t)) return 0;
|
||||||
|
if (/开场白/.test(t)) return 20;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function effectiveDictateOrder(p: Pick<DictateProduct, "tag" | "order">): number {
|
||||||
|
return typeof p.order === "number" && Number.isFinite(p.order)
|
||||||
|
? p.order
|
||||||
|
: defaultDictateOrder(p.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按相对序升序:负 → 0 → 正;同序再按 tag */
|
||||||
|
export function compareDictateProducts(
|
||||||
|
a: Pick<DictateProduct, "tag" | "order">,
|
||||||
|
b: Pick<DictateProduct, "tag" | "order">,
|
||||||
|
): number {
|
||||||
|
const d = effectiveDictateOrder(a) - effectiveDictateOrder(b);
|
||||||
|
if (d !== 0) return d;
|
||||||
|
return a.tag.localeCompare(b.tag, "zh");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortDictateProducts<T extends Pick<DictateProduct, "tag" | "order">>(
|
||||||
|
products: T[],
|
||||||
|
): T[] {
|
||||||
|
return [...products].sort(compareDictateProducts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 insert 的 order;非法则 undefined(表示沿用/默认) */
|
||||||
|
export function parseDictateOrder(raw: unknown): number | undefined {
|
||||||
|
if (raw === undefined || raw === null || raw === "") return undefined;
|
||||||
|
const n = typeof raw === "number" ? raw : Number(raw);
|
||||||
|
if (!Number.isFinite(n)) return undefined;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildDictateMessages,
|
||||||
|
buildDictateSystemPrompt,
|
||||||
|
defaultDictateOrder,
|
||||||
|
extractDictateDialogue,
|
||||||
|
isAllowedProductTag,
|
||||||
|
isDictateModeValue,
|
||||||
|
sortDictateProducts,
|
||||||
|
} from "../src/dictate/index.js";
|
||||||
|
|
||||||
|
describe("dictate context", () => {
|
||||||
|
it("assembles full dialogue + products with almost no filtering", () => {
|
||||||
|
const messages = buildDictateMessages({
|
||||||
|
systemPrompt: "SYS",
|
||||||
|
products: [{ tag: "用户.需求", content: "雨夜压抑" }],
|
||||||
|
dialogue: [
|
||||||
|
{ role: "user", text: "我想要压抑雨夜" },
|
||||||
|
{ role: "assistant", text: "已写入用户.需求" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(messages[0]).toEqual({ role: "system", content: "SYS" });
|
||||||
|
expect(messages[1]?.role).toBe("user");
|
||||||
|
const body = messages[1]?.content ?? "";
|
||||||
|
expect(body).toContain("用户.需求");
|
||||||
|
expect(body).toContain("雨夜压抑");
|
||||||
|
expect(body).toContain("我想要压抑雨夜");
|
||||||
|
expect(body).toContain("已写入用户.需求");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("after clear (empty dialogue) still carries products", () => {
|
||||||
|
const messages = buildDictateMessages({
|
||||||
|
systemPrompt: "SYS",
|
||||||
|
products: [{ tag: "设计.美学纲领", content: "冷色短句" }],
|
||||||
|
dialogue: [],
|
||||||
|
});
|
||||||
|
const body = messages[1]?.content ?? "";
|
||||||
|
expect(body).toContain("冷色短句");
|
||||||
|
expect(body).toContain("尚无对话");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts only user + dictate_reply turns", () => {
|
||||||
|
const turns = extractDictateDialogue([
|
||||||
|
{ kind: "orchestrator_prompt", text: "欢迎" },
|
||||||
|
{ kind: "user_input", text: "你好" },
|
||||||
|
{ kind: "agent_tool", text: "insert" },
|
||||||
|
{ kind: "dictate_reply", text: "已记下" },
|
||||||
|
]);
|
||||||
|
expect(turns).toEqual([
|
||||||
|
{ role: "user", text: "你好" },
|
||||||
|
{ role: "assistant", text: "已记下" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates product tags and mode value", () => {
|
||||||
|
expect(isAllowedProductTag("用户.需求")).toBe(true);
|
||||||
|
expect(isAllowedProductTag("设计.美学纲领")).toBe(true);
|
||||||
|
expect(isAllowedProductTag("设计.正文组成")).toBe(true);
|
||||||
|
expect(isAllowedProductTag("设计.开场白")).toBe(true);
|
||||||
|
expect(isAllowedProductTag("变量.当前")).toBe(false);
|
||||||
|
expect(isDictateModeValue("dictate")).toBe(true);
|
||||||
|
expect(isDictateModeValue("recipe")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("system prompt requires insert toolcall and relative order", () => {
|
||||||
|
const prompt = buildDictateSystemPrompt({
|
||||||
|
recipeName: "正文组成",
|
||||||
|
recipeBrief: "钉呈现壳与可见块",
|
||||||
|
});
|
||||||
|
expect(prompt).toContain("设计.正文组成");
|
||||||
|
expect(prompt).toContain("设计.开场白");
|
||||||
|
expect(prompt).toContain("输出.开场白");
|
||||||
|
expect(prompt).toContain("insert");
|
||||||
|
expect(prompt).toMatch(/order/);
|
||||||
|
expect(prompt).toContain("仅允许 toolcall");
|
||||||
|
expect(prompt).toContain("变化越频繁");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sorts products by relative order: neg then 0 then pos", () => {
|
||||||
|
const sorted = sortDictateProducts([
|
||||||
|
{ tag: "设计.开场白", content: "开", order: 20 },
|
||||||
|
{ tag: "用户.需求", content: "需", order: -40 },
|
||||||
|
{ tag: "设计.正文组成", content: "格", order: 0 },
|
||||||
|
]);
|
||||||
|
expect(sorted.map((p) => p.tag)).toEqual([
|
||||||
|
"用户.需求",
|
||||||
|
"设计.正文组成",
|
||||||
|
"设计.开场白",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default order puts stable/important before volatile", () => {
|
||||||
|
expect(defaultDictateOrder("用户.需求")).toBeLessThan(
|
||||||
|
defaultDictateOrder("设计.正文组成"),
|
||||||
|
);
|
||||||
|
expect(defaultDictateOrder("设计.正文组成")).toBeLessThan(
|
||||||
|
defaultDictateOrder("设计.开场白"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("context lists products in order", () => {
|
||||||
|
const body =
|
||||||
|
buildDictateMessages({
|
||||||
|
systemPrompt: "SYS",
|
||||||
|
products: [
|
||||||
|
{ tag: "设计.开场白", content: "尾", order: 10 },
|
||||||
|
{ tag: "用户.需求", content: "头", order: -10 },
|
||||||
|
],
|
||||||
|
dialogue: [],
|
||||||
|
})[1]?.content ?? "";
|
||||||
|
expect(body.indexOf("用户.需求")).toBeLessThan(body.indexOf("设计.开场白"));
|
||||||
|
expect(body).toContain("order=-10");
|
||||||
|
expect(body).toContain("order=10");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user