Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions packages/intelligence/src/hosted-llm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,20 +1127,25 @@ describe("hosted Agent product-event composition", () => {
expect(requestBodies[1]).toContain("这两组都很适合有创造力的你");
});

it("rejects a verified candidate message that drops an option number", async () => {
it("restores a grounded fallback when verification drops an option number", async () => {
stubChatResponses(
{
content: leftFrame("TOMEET 组局邀请", ["两个候选已经整理好。"]),
optionPreviews: [{ optionNumber: 1, text: "候选一" }, { optionNumber: 2, text: "候选二" }]
},
{
content: leftFrame("TOMEET 组局邀请", ["候选已经整理好。"]),
content: "候选已经整理好。",
optionPreviews: [{ optionNumber: 1, text: "候选一" }]
}
);

await expect(hostedWithSearch().composeProductMessage(agentContext(), matchOptionsEvent))
.rejects.toThrow("候选文案没有覆盖全部选项");
const result = await hostedWithSearch().composeProductMessage(agentContext(), matchOptionsEvent);

expect(result.optionPreviews.map((option) => option.optionNumber)).toEqual([1, 2]);
expect(result.optionPreviews[1]?.text).toContain("共同散步");
expect(result.optionPreviews[1]?.text).toContain("边走边聊");
expect(result.content).toContain("┃ TOMEET 组局邀请");
expect(result.content).toContain("2|共同散步");
});

it("drops option previews on non-candidate product events", async () => {
Expand All @@ -1158,7 +1163,7 @@ describe("hosted Agent product-event composition", () => {
expect(result.optionPreviews).toEqual([]);
});

it("requires invitation and confirmation cards to omit the right border", async () => {
it("repairs invitation cards that include a right border", async () => {
const invalidRightFrame = [
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓",
"┃ TOMEET 组局邀请 ┃",
Expand All @@ -1171,7 +1176,7 @@ describe("hosted Agent product-event composition", () => {
{ content: invalidRightFrame, optionPreviews: [{ optionNumber: 1, text: "故事交换桌" }] }
);

await expect(hostedWithSearch().composeProductMessage(agentContext(), {
const result = await hostedWithSearch().composeProductMessage(agentContext(), {
kind: "match_options",
facts: {
options: [{
Expand All @@ -1182,7 +1187,10 @@ describe("hosted Agent product-event composition", () => {
possibleFacts: []
}]
}
})).rejects.toThrow("必须使用无右边框的左框字符卡片");
});

expect(result.content).toContain("┃ TOMEET 组局邀请");
expect(result.content).not.toMatch(/[┃│┫┤┓┐┛┘]\s*$/mu);
});

it("accepts a left-frame-only formed-room confirmation", async () => {
Expand Down
65 changes: 58 additions & 7 deletions packages/intelligence/src/hosted-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,28 @@ function assertLeftFrameContent(event: AgentProductEvent, content: string): void
}
}

function buildGroundedMatchOptionsFrame(optionPreviews: AgentProductMessage["optionPreviews"]): string {
const body = optionPreviews.flatMap((preview) => {
const text = preview.text
.replace(/```/gu, "")
.replace(/\p{Extended_Pictographic}(?:\uFE0F|\u200D\p{Extended_Pictographic})*/gu, "")
.split(/\r?\n/gu)
.map((line) => line
.replace(/^[\s┏┓┗┛┣┫┃│┌┐└┘├┤]+/gu, "")
.replace(/[┃│┫┤┓┐┛┘]\s*$/gu, "")
.trim())
.filter(Boolean);
return text.map((line, index) => index === 0 ? `${preview.optionNumber}|${line}` : line);
});
return [
"┏━━━━━━━━━━━━",
"┃ TOMEET 组局邀请",
"┣━━━━━━━━━━━━",
...body.map((line) => `┃ ${line}`),
"┗━━━━━━━━━━━━"
].join("\n");
}

export interface HostedLlmOptions {
apiKey: string;
baseUrl: string;
Expand Down Expand Up @@ -1258,17 +1280,46 @@ export class HostedLlmIntelligence implements AgentIntelligence, MatchmakingInte
{ eventKind: event.kind, expectedOptionNumbers },
{ stage: "agent_event.verification", normalize: normalizeProductMessage }
);
let finalized = result;
if (event.kind === "match_options") {
const actual = result.optionPreviews.map((option) => option.optionNumber).sort();
const expected = [...expectedOptionNumbers].sort();
if (actual.length !== expected.length || actual.some((number, index) => number !== expected[index])) {
throw new Error("候选文案没有覆盖全部选项");
}
const verifiedByNumber = new Map(
result.optionPreviews.map((preview) => [preview.optionNumber, preview])
);
const factsByNumber = new Map(
(Array.isArray(event.facts.options) ? event.facts.options : [])
.filter(isRecord)
.map((option) => [Number(option.optionNumber), option] as const)
);
finalized = {
...result,
optionPreviews: expectedOptionNumbers.map((optionNumber) => {
const verified = verifiedByNumber.get(optionNumber);
if (verified) return verified;
const facts = factsByNumber.get(optionNumber);
const activityName = typeof facts?.activityName === "string" ? facts.activityName.trim() : "";
const activityDescription = typeof facts?.activityDescription === "string"
? facts.activityDescription.trim()
: "";
return {
optionNumber,
text: [activityName, activityDescription].filter(Boolean).join(":") || `Option ${optionNumber}`
};
})
};
} else if (result.optionPreviews.length > 0) {
throw new Error("非候选事件不能返回 optionPreviews");
}
assertLeftFrameContent(event, result.content);
return result;
try {
assertLeftFrameContent(event, finalized.content);
} catch (error) {
if (event.kind !== "match_options") throw error;
finalized = {
...finalized,
content: buildGroundedMatchOptionsFrame(finalized.optionPreviews)
};
assertLeftFrameContent(event, finalized.content);
}
return finalized;
}

async summarizeConversation(previousSummary: string, messages: Message[]): Promise<string> {
Expand Down
Loading