Merge pull request #964 from pikasTech/fix/issue959-markdown-fidelity
fix: preserve structured markdown final response
This commit is contained in:
@@ -94,6 +94,36 @@ test("mergeTraceResults prefers terminal result markdown over non-final flattene
|
||||
assert.doesNotMatch(merged.assistantText ?? "", /^I'll read context first/u);
|
||||
});
|
||||
|
||||
test("mergeTraceResults prefers structured result markdown over flattened trace finalResponse", () => {
|
||||
const resultMarkdown = "ISSUE959_MARKDOWN_OK\n\n## hwpod-spec 机制说明\n\n- 第一条\n- 第二条\n\n```bash\nhwpod-cli run --spec .hwlab/hwpod-spec.yaml\n```";
|
||||
const flattenedFinalResponse = "Let me read context first.ISSUE959_MARKDOWN_OK ## hwpod-spec 机制说明 - 第一条 - 第二条 ```bash hwpod-cli run --spec .hwlab/hwpod-spec.yaml ```";
|
||||
const terminal: AgentChatResultResponse = {
|
||||
status: "completed",
|
||||
traceId: "trc_issue959_finalresponse_flattened",
|
||||
assistantText: flattenedFinalResponse,
|
||||
reply: { role: "assistant", content: resultMarkdown }
|
||||
};
|
||||
const events: TraceEvent[] = [
|
||||
{ seq: 31, label: "agentrun:assistant:message", type: "assistant", status: "running", message: flattenedFinalResponse },
|
||||
{ seq: 36, label: "agentrun:assistant:message", type: "assistant", status: "running", message: flattenedFinalResponse },
|
||||
{ seq: 42, label: "agentrun:result:completed", type: "result", status: "completed", terminal: true, message: "AgentRun result is ready for HWLAB short-connection polling." }
|
||||
];
|
||||
const trace: TraceSnapshot = {
|
||||
traceId: "trc_issue959_finalresponse_flattened",
|
||||
status: "completed",
|
||||
events,
|
||||
eventCount: events.length,
|
||||
finalResponse: { text: flattenedFinalResponse },
|
||||
lastEventLabel: "agentrun:result:completed"
|
||||
};
|
||||
|
||||
const merged = mergeTraceResults(terminal, trace);
|
||||
|
||||
assert.equal(merged.assistantText, resultMarkdown);
|
||||
assert.match(merged.assistantText ?? "", /ISSUE959_MARKDOWN_OK\n\n## hwpod-spec/u);
|
||||
assert.doesNotMatch(merged.assistantText ?? "", /^Let me read context first/u);
|
||||
});
|
||||
|
||||
test("mergeTraceResults prefers persisted finalResponse text for expired or compact natural traces", () => {
|
||||
const finalMarkdown = "**结论**\n\n- 自然结束和刷新后都应该显示这一段。\n- Markdown 换行必须保留。";
|
||||
const terminal: AgentChatResultResponse = {
|
||||
|
||||
@@ -97,10 +97,12 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac
|
||||
const terminalRunnerEvents = Array.isArray(terminalRunnerTrace?.events) ? terminalRunnerTrace.events : [];
|
||||
const events = longestTraceEvents(traceEvents, terminalEvents, terminalRunnerEvents);
|
||||
const mergedTrace = { ...trace, events, eventCount: trace.eventCount ?? events.length };
|
||||
const assistantText = finalResponseText(trace.finalResponse)
|
||||
?? terminalAssistantTextFromTraceEvents(events)
|
||||
?? firstNonEmptyResultText(terminal)
|
||||
?? assistantTextFromTraceEvents(events);
|
||||
const assistantText = bestAssistantTextCandidate([
|
||||
{ text: finalResponseText(trace.finalResponse), priority: 40 },
|
||||
{ text: terminalAssistantTextFromTraceEvents(events), priority: 30 },
|
||||
{ text: firstNonEmptyResultText(terminal), priority: 20 },
|
||||
{ text: assistantTextFromTraceEvents(events), priority: 10 }
|
||||
]);
|
||||
return {
|
||||
...terminal,
|
||||
traceId: trace.traceId ?? terminal.traceId,
|
||||
@@ -144,7 +146,53 @@ function mergeTerminalRunnerTrace(terminalTrace: AgentChatResultResponse["runner
|
||||
function firstNonEmptyResultText(result: AgentChatResultResponse): string | null {
|
||||
const reply = result.reply;
|
||||
const replyText = typeof reply === "string" ? reply : typeof reply?.content === "string" ? reply.content : null;
|
||||
return firstNonEmptyString(result.assistantText, replyText);
|
||||
return bestAssistantTextCandidate([
|
||||
{ text: result.assistantText, priority: 20 },
|
||||
{ text: replyText, priority: 10 }
|
||||
]);
|
||||
}
|
||||
|
||||
interface AssistantTextCandidate {
|
||||
text: string | null | undefined;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
function bestAssistantTextCandidate(candidates: AssistantTextCandidate[]): string | null {
|
||||
const scored: Array<{ text: string; fidelity: number; priority: number; index: number }> = [];
|
||||
candidates.forEach((candidate, index) => {
|
||||
const text = firstNonEmptyString(candidate.text);
|
||||
if (!text) return;
|
||||
scored.push({ text, fidelity: markdownBlockFidelityScore(text), priority: candidate.priority, index });
|
||||
});
|
||||
const best = scored.reduce<typeof scored[number] | null>((currentBest, current) => {
|
||||
if (!currentBest || compareAssistantTextCandidate(current, currentBest) > 0) return current;
|
||||
return currentBest;
|
||||
}, null);
|
||||
return best?.text ?? null;
|
||||
}
|
||||
|
||||
function compareAssistantTextCandidate(
|
||||
left: { fidelity: number; priority: number; index: number },
|
||||
right: { fidelity: number; priority: number; index: number }
|
||||
): number {
|
||||
if (left.fidelity !== right.fidelity) return left.fidelity - right.fidelity;
|
||||
if (left.priority !== right.priority) return left.priority - right.priority;
|
||||
return right.index - left.index;
|
||||
}
|
||||
|
||||
function markdownBlockFidelityScore(text: string): number {
|
||||
const newlineCount = (text.match(/\n/gu) ?? []).length;
|
||||
const headingCount = (text.match(/^#{1,6}\s+/gmu) ?? []).length;
|
||||
const listItemCount = (text.match(/^\s*(?:[-*+] |\d+\.\s+)/gmu) ?? []).length;
|
||||
const codeFenceCount = (text.match(/^```/gmu) ?? []).length;
|
||||
const tableRowCount = (text.match(/^\s*\|.*\|\s*$/gmu) ?? []).length;
|
||||
const paragraphBreakCount = (text.match(/\n\s*\n/gu) ?? []).length;
|
||||
return Math.min(newlineCount, 24)
|
||||
+ headingCount * 8
|
||||
+ listItemCount * 4
|
||||
+ codeFenceCount * 8
|
||||
+ tableRowCount * 4
|
||||
+ paragraphBreakCount * 3;
|
||||
}
|
||||
|
||||
function assistantTextFromTraceEvents(events: TraceEvent[]): string | null {
|
||||
|
||||
Reference in New Issue
Block a user