@oneuptime/common 11.5.8 → 11.5.9
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.
- package/Models/DatabaseModels/AIAgentTaskPullRequest.ts +15 -6
- package/Models/DatabaseModels/AIRun.ts +37 -0
- package/Models/DatabaseModels/AIRunEvent.ts +34 -1
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +215 -0
- package/Models/DatabaseModels/Project.ts +25 -0
- package/Models/DatabaseModels/StatusPage.ts +42 -0
- package/Server/API/AIAgentTaskAPI.ts +38 -12
- package/Server/API/AIAgentTaskLogAPI.ts +49 -0
- package/Server/API/AIChatAPI.ts +16 -0
- package/Server/API/AIReadinessAPI.ts +84 -0
- package/Server/API/CodeFixRunAPI.ts +182 -0
- package/Server/API/TelemetryExceptionAPI.ts +6 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.ts +95 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/AIRunEventService.ts +15 -1
- package/Server/Services/AIRunService.ts +38 -1
- package/Server/Services/LlmProviderService.ts +2 -0
- package/Server/Services/ProjectService.ts +42 -0
- package/Server/Services/StatusPageService.ts +87 -0
- package/Server/Services/TelemetryExceptionService.ts +19 -90
- package/Server/Types/Markdown.ts +9 -9
- package/Server/Types/MarkdownSlugify.ts +47 -0
- package/Server/Utils/AI/AIRunTranscript.ts +240 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +9 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +119 -5
- package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +75 -0
- package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +235 -0
- package/Server/Utils/AI/SRE/Insights/FixRouting.ts +2 -3
- package/Server/Utils/AI/Toolbox/CodeTools.ts +883 -0
- package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +519 -0
- package/Server/Utils/AI/Toolbox/Index.ts +27 -0
- package/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.ts +279 -0
- package/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.ts +19 -1
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +600 -0
- package/Server/Utils/CodeRepository/StackTraceRepoResolver.ts +7 -2
- package/Server/Utils/LLM/LLMService.ts +397 -18
- package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +4 -11
- package/Tests/Models/StatusPageEnableMcpServer.test.ts +125 -0
- package/Tests/Server/Services/StatusPageServiceMcp.test.ts +223 -0
- package/Tests/Server/Services/TelemetryExceptionAIFixReadiness.test.ts +26 -3
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +12 -0
- package/Tests/Server/Types/Markdown.test.ts +106 -0
- package/Tests/Server/Utils/AI/AIRunTranscript.test.ts +182 -0
- package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +411 -0
- package/Tests/Server/Utils/AI/CodeTools.test.ts +596 -0
- package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +475 -0
- package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +548 -0
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +142 -0
- package/Tests/Server/Utils/AI/ScheduledMaintenanceTools.test.ts +176 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.test.ts +63 -0
- package/Tests/Server/Utils/GitHubGetFileContent.test.ts +197 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +502 -0
- package/Tests/Types/AI/AIAgentTaskStatus.test.ts +7 -6
- package/Tests/Types/AI/AIChatPageContext.test.ts +0 -0
- package/Tests/Types/AI/AIRunStatus.test.ts +42 -0
- package/Tests/Types/Log/LogSeverity.test.ts +78 -0
- package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +167 -0
- package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +66 -0
- package/Tests/UI/Utils/AIChatExport/ChatWidgetData.test.ts +319 -0
- package/Tests/UI/Utils/AIChatExport/ChatWidgetMarkdown.test.ts +188 -0
- package/Types/AI/AIAgentTaskStatus.ts +13 -0
- package/Types/AI/AIChatPageContext.ts +137 -0
- package/Types/AI/AIChatTypes.ts +44 -0
- package/Types/AI/AIFixReadiness.ts +46 -0
- package/Types/AI/AIRunStatus.ts +11 -0
- package/Types/Log/LogSeverity.ts +61 -0
- package/Types/Monitor/SnmpMonitor/SnmpVersion.ts +40 -0
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -2
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +3 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +8 -2
- package/UI/Components/Forms/BasicForm.tsx +26 -8
- package/UI/Styles/Theme.css +1314 -0
- package/UI/Utils/AIChatExport/ChatWidgetData.ts +316 -0
- package/UI/Utils/AIChatExport/ChatWidgetMarkdown.ts +349 -0
- package/UI/Utils/AIChatExport/ConversationMarkdown.ts +177 -0
- package/UI/Utils/AIChatExport/MarkdownBlocks.ts +327 -0
- package/UI/Utils/AIChatExport/MarkdownSafety.ts +215 -0
- package/UI/Utils/DownloadFile.ts +54 -0
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js +15 -7
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIRun.js +38 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +31 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +225 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +27 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +43 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskAPI.js +37 -11
- package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskLogAPI.js +32 -7
- package/build/dist/Server/API/AIAgentTaskLogAPI.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +9 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIReadinessAPI.js +57 -0
- package/build/dist/Server/API/AIReadinessAPI.js.map +1 -0
- package/build/dist/Server/API/CodeFixRunAPI.js +142 -0
- package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryExceptionAPI.js +4 -0
- package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js +80 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIRunEventService.js +8 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +29 -0
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +2 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +39 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +81 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +14 -61
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +8 -8
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/MarkdownSlugify.js +47 -0
- package/build/dist/Server/Types/MarkdownSlugify.js.map +1 -0
- package/build/dist/Server/Utils/AI/AIRunTranscript.js +180 -0
- package/build/dist/Server/Utils/AI/AIRunTranscript.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +103 -5
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +55 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +211 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +654 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +389 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +19 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js +239 -0
- package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js +19 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +439 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js +1 -1
- package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +275 -19
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +2 -11
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
- package/build/dist/Types/AI/AIAgentTaskStatus.js +12 -0
- package/build/dist/Types/AI/AIAgentTaskStatus.js.map +1 -1
- package/build/dist/Types/AI/AIChatPageContext.js +101 -0
- package/build/dist/Types/AI/AIChatPageContext.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +2 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
- package/build/dist/Types/AI/AIFixReadiness.js +9 -0
- package/build/dist/Types/AI/AIFixReadiness.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +11 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -1
- package/build/dist/Types/Log/LogSeverity.js +56 -0
- package/build/dist/Types/Log/LogSeverity.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js +38 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +9 -2
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +3 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +9 -2
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +24 -6
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js +217 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js +234 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +111 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js +233 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js +134 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js.map +1 -0
- package/build/dist/UI/Utils/DownloadFile.js +41 -0
- package/build/dist/UI/Utils/DownloadFile.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import AIConversationMessage from "../../../Models/DatabaseModels/AIConversationMessage";
|
|
2
|
+
import AIRun from "../../../Models/DatabaseModels/AIRun";
|
|
3
|
+
import AIChatMessageRole from "../../../Types/AI/AIChatMessageRole";
|
|
4
|
+
import AIChatMessageStatus from "../../../Types/AI/AIChatMessageStatus";
|
|
5
|
+
import {
|
|
6
|
+
AIChatCitation,
|
|
7
|
+
AIChatToolAction,
|
|
8
|
+
AIChatWidget,
|
|
9
|
+
} from "../../../Types/AI/AIChatTypes";
|
|
10
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
11
|
+
import neutralizeAssistantMarkdown from "./MarkdownSafety";
|
|
12
|
+
import {
|
|
13
|
+
citationsToMarkdown,
|
|
14
|
+
toolActionsToMarkdown,
|
|
15
|
+
widgetsToMarkdown,
|
|
16
|
+
} from "./ChatWidgetMarkdown";
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* Renders a whole AI chat conversation as a markdown document.
|
|
20
|
+
*
|
|
21
|
+
* The shape follows the chat itself: alternating turns, each assistant turn
|
|
22
|
+
* carrying the widgets, actions and citations that were shown beneath it. What
|
|
23
|
+
* the reader gets is the transcript plus the evidence behind it, in a file that
|
|
24
|
+
* reads fine in a plain text editor.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface ConversationMarkdownOptions {
|
|
28
|
+
title: string;
|
|
29
|
+
messages: Array<AIConversationMessage>;
|
|
30
|
+
latestRun?: AIRun | undefined;
|
|
31
|
+
exportedAt: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Mirrors the cost footer the chat shows under the newest completed answer.
|
|
35
|
+
function runFooter(run: AIRun): string {
|
|
36
|
+
const parts: Array<string> = [];
|
|
37
|
+
|
|
38
|
+
if (run.egressManifest?.modelName) {
|
|
39
|
+
parts.push(run.egressManifest.modelName);
|
|
40
|
+
}
|
|
41
|
+
if (run.totalTokens) {
|
|
42
|
+
parts.push(`${run.totalTokens.toLocaleString()} tokens`);
|
|
43
|
+
}
|
|
44
|
+
if (run.totalCostInUSDCents) {
|
|
45
|
+
parts.push(`$${(run.totalCostInUSDCents / 100).toFixed(4)}`);
|
|
46
|
+
}
|
|
47
|
+
const toolCallCount: number = run.toolCallCount || 0;
|
|
48
|
+
parts.push(`${toolCallCount} ${toolCallCount === 1 ? "query" : "queries"}`);
|
|
49
|
+
|
|
50
|
+
return parts.join(" · ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function messageToMarkdown(
|
|
54
|
+
message: AIConversationMessage,
|
|
55
|
+
isLastCompletedAssistant: boolean,
|
|
56
|
+
latestRun: AIRun | undefined,
|
|
57
|
+
): Array<string> {
|
|
58
|
+
const lines: Array<string> = [];
|
|
59
|
+
const timestamp: string = message.createdAt
|
|
60
|
+
? OneUptimeDate.getDateAsLocalFormattedString(message.createdAt)
|
|
61
|
+
: "";
|
|
62
|
+
|
|
63
|
+
if (message.role === AIChatMessageRole.User) {
|
|
64
|
+
lines.push("## You", "");
|
|
65
|
+
/*
|
|
66
|
+
* The user's own words, shown literally on screen. No neutralization: this
|
|
67
|
+
* is the operator's own input, not model output derived from telemetry.
|
|
68
|
+
*/
|
|
69
|
+
if (message.contentInMarkdown) {
|
|
70
|
+
lines.push(message.contentInMarkdown.trim(), "");
|
|
71
|
+
}
|
|
72
|
+
if (timestamp) {
|
|
73
|
+
lines.push(`_${timestamp}_`, "");
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push("## AI", "");
|
|
79
|
+
|
|
80
|
+
if (message.status === AIChatMessageStatus.Error) {
|
|
81
|
+
lines.push(
|
|
82
|
+
`> **Error:** ${
|
|
83
|
+
message.errorMessage || "Something went wrong generating this response."
|
|
84
|
+
}`,
|
|
85
|
+
"",
|
|
86
|
+
);
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (message.contentInMarkdown) {
|
|
91
|
+
lines.push(neutralizeAssistantMarkdown(message.contentInMarkdown), "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const widgets: Array<AIChatWidget> = message.widgets || [];
|
|
95
|
+
if (widgets.length > 0) {
|
|
96
|
+
lines.push(widgetsToMarkdown(widgets), "");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const toolActions: Array<AIChatToolAction> = message.toolActions || [];
|
|
100
|
+
if (toolActions.length > 0) {
|
|
101
|
+
lines.push(toolActionsToMarkdown(toolActions), "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const citations: Array<AIChatCitation> = message.citations || [];
|
|
105
|
+
if (citations.length > 0) {
|
|
106
|
+
lines.push(citationsToMarkdown(citations), "");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const footerParts: Array<string> = [];
|
|
110
|
+
if (timestamp) {
|
|
111
|
+
footerParts.push(timestamp);
|
|
112
|
+
}
|
|
113
|
+
if (isLastCompletedAssistant && latestRun) {
|
|
114
|
+
footerParts.push(runFooter(latestRun));
|
|
115
|
+
}
|
|
116
|
+
if (footerParts.length > 0) {
|
|
117
|
+
lines.push(`_${footerParts.join(" · ")}_`, "");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default function convertConversationToMarkdown(
|
|
124
|
+
options: ConversationMarkdownOptions,
|
|
125
|
+
): string {
|
|
126
|
+
const lines: Array<string> = [
|
|
127
|
+
`# ${options.title || "AI conversation"}`,
|
|
128
|
+
"",
|
|
129
|
+
`_Exported from OneUptime on ${OneUptimeDate.getDateAsLocalFormattedString(
|
|
130
|
+
options.exportedAt,
|
|
131
|
+
)}_`,
|
|
132
|
+
"",
|
|
133
|
+
"---",
|
|
134
|
+
"",
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* The run footer belongs to the newest completed answer, matching where the
|
|
139
|
+
* chat shows it.
|
|
140
|
+
*/
|
|
141
|
+
const lastCompletedAssistantId: string | undefined = [...options.messages]
|
|
142
|
+
.reverse()
|
|
143
|
+
.find((message: AIConversationMessage) => {
|
|
144
|
+
return (
|
|
145
|
+
message.role === AIChatMessageRole.Assistant &&
|
|
146
|
+
message.status === AIChatMessageStatus.Completed
|
|
147
|
+
);
|
|
148
|
+
})
|
|
149
|
+
?.id?.toString();
|
|
150
|
+
|
|
151
|
+
for (const message of options.messages) {
|
|
152
|
+
/*
|
|
153
|
+
* An answer still being generated has nothing to export yet; the chat
|
|
154
|
+
* represents it with a live activity feed rather than content.
|
|
155
|
+
*/
|
|
156
|
+
if (
|
|
157
|
+
message.role === AIChatMessageRole.Assistant &&
|
|
158
|
+
(message.status === AIChatMessageStatus.InProgress ||
|
|
159
|
+
message.status === AIChatMessageStatus.Pending) &&
|
|
160
|
+
!message.contentInMarkdown &&
|
|
161
|
+
(message.widgets || []).length === 0 &&
|
|
162
|
+
(message.toolActions || []).length === 0
|
|
163
|
+
) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
lines.push(
|
|
168
|
+
...messageToMarkdown(
|
|
169
|
+
message,
|
|
170
|
+
message.id?.toString() === lastCompletedAssistantId,
|
|
171
|
+
options.latestRun,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return `${lines.join("\n").trim()}\n`;
|
|
177
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import remarkGfm from "remark-gfm";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import { unified } from "unified";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Flattens markdown into a small list of drawable blocks.
|
|
7
|
+
*
|
|
8
|
+
* The PDF exporter cannot use the chat's React markdown renderer, but it should
|
|
9
|
+
* not hand-roll a markdown parser either — that is how exports end up rendering
|
|
10
|
+
* a code fence as bold text. So the same parser the chat uses (remark + GFM)
|
|
11
|
+
* produces the tree, and this module lowers it into a shape a drawing routine
|
|
12
|
+
* can consume without knowing anything about mdast.
|
|
13
|
+
*
|
|
14
|
+
* Only the constructs an assistant answer actually produces are modelled.
|
|
15
|
+
* Anything unrecognized degrades to its text content rather than disappearing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export enum MarkdownBlockType {
|
|
19
|
+
Heading = "Heading",
|
|
20
|
+
Paragraph = "Paragraph",
|
|
21
|
+
ListItem = "ListItem",
|
|
22
|
+
Code = "Code",
|
|
23
|
+
Quote = "Quote",
|
|
24
|
+
Table = "Table",
|
|
25
|
+
Rule = "Rule",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MarkdownInline {
|
|
29
|
+
text: string;
|
|
30
|
+
isBold?: boolean | undefined;
|
|
31
|
+
isItalic?: boolean | undefined;
|
|
32
|
+
isCode?: boolean | undefined;
|
|
33
|
+
isStrikethrough?: boolean | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MarkdownBlock {
|
|
37
|
+
type: MarkdownBlockType;
|
|
38
|
+
// Heading level 1-6.
|
|
39
|
+
level?: number | undefined;
|
|
40
|
+
inlines?: Array<MarkdownInline> | undefined;
|
|
41
|
+
// Code blocks.
|
|
42
|
+
code?: string | undefined;
|
|
43
|
+
language?: string | undefined;
|
|
44
|
+
// List items.
|
|
45
|
+
indent?: number | undefined;
|
|
46
|
+
marker?: string | undefined;
|
|
47
|
+
// Tables.
|
|
48
|
+
headers?: Array<string> | undefined;
|
|
49
|
+
rows?: Array<Array<string>> | undefined;
|
|
50
|
+
alignments?: Array<"left" | "right" | "center"> | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface MarkdownNode {
|
|
54
|
+
type: string;
|
|
55
|
+
value?: string | undefined;
|
|
56
|
+
lang?: string | undefined;
|
|
57
|
+
depth?: number | undefined;
|
|
58
|
+
ordered?: boolean | undefined;
|
|
59
|
+
start?: number | null | undefined;
|
|
60
|
+
align?: Array<string | null> | undefined;
|
|
61
|
+
children?: Array<MarkdownNode> | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface InlineStyle {
|
|
65
|
+
isBold: boolean;
|
|
66
|
+
isItalic: boolean;
|
|
67
|
+
isCode: boolean;
|
|
68
|
+
isStrikethrough: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const BASE_STYLE: InlineStyle = {
|
|
72
|
+
isBold: false,
|
|
73
|
+
isItalic: false,
|
|
74
|
+
isCode: false,
|
|
75
|
+
isStrikethrough: false,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function collectInlines(
|
|
79
|
+
nodes: Array<MarkdownNode>,
|
|
80
|
+
style: InlineStyle,
|
|
81
|
+
into: Array<MarkdownInline>,
|
|
82
|
+
): void {
|
|
83
|
+
for (const node of nodes) {
|
|
84
|
+
switch (node.type) {
|
|
85
|
+
case "text":
|
|
86
|
+
if (node.value) {
|
|
87
|
+
into.push({
|
|
88
|
+
text: node.value,
|
|
89
|
+
isBold: style.isBold,
|
|
90
|
+
isItalic: style.isItalic,
|
|
91
|
+
isStrikethrough: style.isStrikethrough,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case "inlineCode":
|
|
97
|
+
into.push({
|
|
98
|
+
text: node.value || "",
|
|
99
|
+
isCode: true,
|
|
100
|
+
isBold: style.isBold,
|
|
101
|
+
isItalic: style.isItalic,
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case "strong":
|
|
106
|
+
collectInlines(node.children || [], { ...style, isBold: true }, into);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case "emphasis":
|
|
110
|
+
collectInlines(node.children || [], { ...style, isItalic: true }, into);
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case "delete":
|
|
114
|
+
collectInlines(
|
|
115
|
+
node.children || [],
|
|
116
|
+
{ ...style, isStrikethrough: true },
|
|
117
|
+
into,
|
|
118
|
+
);
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case "break":
|
|
122
|
+
into.push({ text: "\n" });
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* Links and images should already have been rewritten into text by the
|
|
127
|
+
* neutralizer before this runs. If one survives, keep its text and drop
|
|
128
|
+
* the destination rather than carrying a live URL forward.
|
|
129
|
+
*/
|
|
130
|
+
case "link":
|
|
131
|
+
case "image":
|
|
132
|
+
case "linkReference":
|
|
133
|
+
case "imageReference":
|
|
134
|
+
collectInlines(node.children || [], style, into);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
if (node.children) {
|
|
139
|
+
collectInlines(node.children, style, into);
|
|
140
|
+
} else if (node.value) {
|
|
141
|
+
into.push({ text: node.value, isBold: style.isBold });
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function inlinesOf(nodes: Array<MarkdownNode>): Array<MarkdownInline> {
|
|
149
|
+
const inlines: Array<MarkdownInline> = [];
|
|
150
|
+
collectInlines(nodes, BASE_STYLE, inlines);
|
|
151
|
+
return inlines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function plainTextOf(nodes: Array<MarkdownNode>): string {
|
|
155
|
+
return inlinesOf(nodes)
|
|
156
|
+
.map((inline: MarkdownInline) => {
|
|
157
|
+
return inline.text;
|
|
158
|
+
})
|
|
159
|
+
.join("");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function tableToBlock(node: MarkdownNode): MarkdownBlock {
|
|
163
|
+
const rows: Array<Array<string>> = (node.children || []).map(
|
|
164
|
+
(row: MarkdownNode) => {
|
|
165
|
+
return (row.children || []).map((cell: MarkdownNode) => {
|
|
166
|
+
return plainTextOf(cell.children || []);
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const headers: Array<string> =
|
|
172
|
+
rows.length > 0 ? (rows[0] as Array<string>) : [];
|
|
173
|
+
|
|
174
|
+
const alignments: Array<"left" | "right" | "center"> = (node.align || []).map(
|
|
175
|
+
(align: string | null) => {
|
|
176
|
+
if (align === "right") {
|
|
177
|
+
return "right";
|
|
178
|
+
}
|
|
179
|
+
if (align === "center") {
|
|
180
|
+
return "center";
|
|
181
|
+
}
|
|
182
|
+
return "left";
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
type: MarkdownBlockType.Table,
|
|
188
|
+
headers: headers,
|
|
189
|
+
rows: rows.slice(1),
|
|
190
|
+
alignments: alignments,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function flattenList(
|
|
195
|
+
node: MarkdownNode,
|
|
196
|
+
indent: number,
|
|
197
|
+
into: Array<MarkdownBlock>,
|
|
198
|
+
): void {
|
|
199
|
+
const isOrdered: boolean = node.ordered === true;
|
|
200
|
+
let counter: number = typeof node.start === "number" ? node.start : 1;
|
|
201
|
+
|
|
202
|
+
for (const item of node.children || []) {
|
|
203
|
+
const inlines: Array<MarkdownInline> = [];
|
|
204
|
+
const nested: Array<MarkdownBlock> = [];
|
|
205
|
+
|
|
206
|
+
for (const child of item.children || []) {
|
|
207
|
+
if (child.type === "list") {
|
|
208
|
+
flattenList(child, indent + 1, nested);
|
|
209
|
+
} else if (child.type === "paragraph") {
|
|
210
|
+
// Multiple paragraphs in one item are joined by a soft break.
|
|
211
|
+
if (inlines.length > 0) {
|
|
212
|
+
inlines.push({ text: "\n" });
|
|
213
|
+
}
|
|
214
|
+
collectInlines(child.children || [], BASE_STYLE, inlines);
|
|
215
|
+
} else {
|
|
216
|
+
toBlocks(child, nested, indent + 1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
into.push({
|
|
221
|
+
type: MarkdownBlockType.ListItem,
|
|
222
|
+
inlines: inlines,
|
|
223
|
+
indent: indent,
|
|
224
|
+
marker: isOrdered ? `${counter}.` : "-",
|
|
225
|
+
});
|
|
226
|
+
into.push(...nested);
|
|
227
|
+
|
|
228
|
+
counter++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function toBlocks(
|
|
233
|
+
node: MarkdownNode,
|
|
234
|
+
into: Array<MarkdownBlock>,
|
|
235
|
+
indent: number = 0,
|
|
236
|
+
): void {
|
|
237
|
+
switch (node.type) {
|
|
238
|
+
case "heading":
|
|
239
|
+
into.push({
|
|
240
|
+
type: MarkdownBlockType.Heading,
|
|
241
|
+
level: node.depth || 1,
|
|
242
|
+
inlines: inlinesOf(node.children || []),
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
245
|
+
|
|
246
|
+
case "paragraph":
|
|
247
|
+
into.push({
|
|
248
|
+
type: MarkdownBlockType.Paragraph,
|
|
249
|
+
inlines: inlinesOf(node.children || []),
|
|
250
|
+
indent: indent,
|
|
251
|
+
});
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case "list":
|
|
255
|
+
flattenList(node, indent, into);
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case "code":
|
|
259
|
+
into.push({
|
|
260
|
+
type: MarkdownBlockType.Code,
|
|
261
|
+
code: node.value || "",
|
|
262
|
+
language: node.lang || "",
|
|
263
|
+
});
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case "blockquote":
|
|
267
|
+
for (const child of node.children || []) {
|
|
268
|
+
const quoted: Array<MarkdownBlock> = [];
|
|
269
|
+
toBlocks(child, quoted, indent);
|
|
270
|
+
for (const block of quoted) {
|
|
271
|
+
into.push({ ...block, type: MarkdownBlockType.Quote });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case "table":
|
|
277
|
+
into.push(tableToBlock(node));
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
case "thematicBreak":
|
|
281
|
+
into.push({ type: MarkdownBlockType.Rule });
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case "html":
|
|
285
|
+
// Raw HTML is inert text here; show it rather than silently dropping it.
|
|
286
|
+
if (node.value) {
|
|
287
|
+
into.push({
|
|
288
|
+
type: MarkdownBlockType.Paragraph,
|
|
289
|
+
inlines: [{ text: node.value }],
|
|
290
|
+
indent: indent,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
|
|
295
|
+
default:
|
|
296
|
+
if (node.children) {
|
|
297
|
+
for (const child of node.children) {
|
|
298
|
+
toBlocks(child, into, indent);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default function parseMarkdownBlocks(
|
|
306
|
+
text: string,
|
|
307
|
+
): Array<MarkdownBlock> {
|
|
308
|
+
if (!text) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const tree: MarkdownNode = unified()
|
|
314
|
+
.use(remarkParse)
|
|
315
|
+
.use(remarkGfm)
|
|
316
|
+
.parse(text) as unknown as MarkdownNode;
|
|
317
|
+
|
|
318
|
+
const blocks: Array<MarkdownBlock> = [];
|
|
319
|
+
for (const child of tree.children || []) {
|
|
320
|
+
toBlocks(child, blocks);
|
|
321
|
+
}
|
|
322
|
+
return blocks;
|
|
323
|
+
} catch {
|
|
324
|
+
// A parse failure should still show the answer, just without formatting.
|
|
325
|
+
return [{ type: MarkdownBlockType.Paragraph, inlines: [{ text: text }] }];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { gfmToMarkdown } from "mdast-util-gfm";
|
|
2
|
+
import { toMarkdown } from "mdast-util-to-markdown";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkParse from "remark-parse";
|
|
5
|
+
import { unified } from "unified";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Neutralizes the exfiltration channels in assistant markdown before it is
|
|
9
|
+
* written to an exported file.
|
|
10
|
+
*
|
|
11
|
+
* The chat UI already does this on screen: SafeChatMarkdown renders through
|
|
12
|
+
* MarkdownViewer in safeMode, which drops every href and never emits an <img>.
|
|
13
|
+
* The reason is that an assistant answer is derived from attacker-influenceable
|
|
14
|
+
* telemetry (a log body or exception message can carry markdown), so a
|
|
15
|
+
* prompt-injected response must not be able to render a tracking pixel or hand
|
|
16
|
+
* the reader a clickable exfil URL. An export that turned those back into live
|
|
17
|
+
* links would reopen that hole in a file that then gets shared around, so the
|
|
18
|
+
* exports hold the same line.
|
|
19
|
+
*
|
|
20
|
+
* Unlike the on-screen path we keep the URL visible — a reader auditing a
|
|
21
|
+
* transcript should be able to see where a link pointed — but only ever as
|
|
22
|
+
* inert text. In markdown that means wrapping it in a code span: a bare URL in
|
|
23
|
+
* a .md file would otherwise be turned back into a live link by GFM's autolink
|
|
24
|
+
* literals, which is precisely what we are trying to prevent.
|
|
25
|
+
*
|
|
26
|
+
* This works on the PARSED tree rather than by regex over the raw string, for
|
|
27
|
+
* the same reason MarkdownViewer does: no CommonMark edge case (reference
|
|
28
|
+
* links, autolinks, raw HTML, nesting) can slip a link past a parser, whereas
|
|
29
|
+
* regexes miss those forms routinely.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* A structural view of the mdast nodes we touch. Declared locally rather than
|
|
34
|
+
* imported from @types/mdast: the version hoisted into Common/node_modules is
|
|
35
|
+
* v3 (pulled in by slackify-markdown's old unified 9 tree) while the mdast-util
|
|
36
|
+
* packages here expect v4, so importing those types would fight the resolver
|
|
37
|
+
* for no benefit.
|
|
38
|
+
*/
|
|
39
|
+
interface MarkdownNode {
|
|
40
|
+
type: string;
|
|
41
|
+
value?: string | undefined;
|
|
42
|
+
url?: string | undefined;
|
|
43
|
+
alt?: string | null | undefined;
|
|
44
|
+
identifier?: string | undefined;
|
|
45
|
+
children?: Array<MarkdownNode> | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type MarkdownRoot = Parameters<typeof toMarkdown>[0];
|
|
49
|
+
|
|
50
|
+
// Node types that may only appear inside a block, never directly under the root.
|
|
51
|
+
const PHRASING_TYPES: Set<string> = new Set(["text", "inlineCode"]);
|
|
52
|
+
|
|
53
|
+
function textNode(value: string): MarkdownNode {
|
|
54
|
+
return { type: "text", value: value };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function codeNode(value: string): MarkdownNode {
|
|
58
|
+
return { type: "inlineCode", value: value };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The visible text of a subtree, used to decide whether a label adds anything.
|
|
62
|
+
function textOf(nodes: Array<MarkdownNode>): string {
|
|
63
|
+
return nodes
|
|
64
|
+
.map((node: MarkdownNode) => {
|
|
65
|
+
if (node.type === "text" || node.type === "inlineCode") {
|
|
66
|
+
return node.value || "";
|
|
67
|
+
}
|
|
68
|
+
return node.children ? textOf(node.children) : "";
|
|
69
|
+
})
|
|
70
|
+
.join("");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* "label (`url`)" — or just "`url`" when the label is only a repeat of the URL,
|
|
75
|
+
* which is what an autolink or a GFM bare URL parses into.
|
|
76
|
+
*/
|
|
77
|
+
function inertLink(
|
|
78
|
+
label: Array<MarkdownNode>,
|
|
79
|
+
url: string,
|
|
80
|
+
): Array<MarkdownNode> {
|
|
81
|
+
const labelText: string = textOf(label);
|
|
82
|
+
const hasDistinctLabel: boolean = Boolean(labelText) && labelText !== url;
|
|
83
|
+
|
|
84
|
+
if (!url) {
|
|
85
|
+
return hasDistinctLabel ? label : [textNode(labelText)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!hasDistinctLabel) {
|
|
89
|
+
return [codeNode(url)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [...label, textNode(" ("), codeNode(url), textNode(")")];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function neutralizeNode(
|
|
96
|
+
node: MarkdownNode,
|
|
97
|
+
definitions: Map<string, string>,
|
|
98
|
+
isRootChild: boolean,
|
|
99
|
+
): Array<MarkdownNode> {
|
|
100
|
+
// Depth first, so a link nested inside emphasis is rewritten before its parent.
|
|
101
|
+
if (node.children) {
|
|
102
|
+
const rewritten: Array<MarkdownNode> = [];
|
|
103
|
+
for (const child of node.children) {
|
|
104
|
+
rewritten.push(...neutralizeNode(child, definitions, false));
|
|
105
|
+
}
|
|
106
|
+
node.children = rewritten;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const identifier: string = (node.identifier || "").toLowerCase();
|
|
110
|
+
|
|
111
|
+
switch (node.type) {
|
|
112
|
+
/*
|
|
113
|
+
* Drop link definitions outright. Their referents have already been
|
|
114
|
+
* inlined below, and leaving a definition behind would let "[x][ref]"
|
|
115
|
+
* resolve to a live link again.
|
|
116
|
+
*/
|
|
117
|
+
case "definition":
|
|
118
|
+
return [];
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* Raw HTML is inert on screen (react-markdown does not render it without
|
|
122
|
+
* rehype-raw), but a .md file is rendered by viewers that do, where an
|
|
123
|
+
* <img src> or <a href> would be live. Demote it to text — toMarkdown
|
|
124
|
+
* escapes the angle brackets on the way out.
|
|
125
|
+
*/
|
|
126
|
+
case "html": {
|
|
127
|
+
const asText: MarkdownNode = textNode(node.value || "");
|
|
128
|
+
return isRootChild
|
|
129
|
+
? [{ type: "paragraph", children: [asText] }]
|
|
130
|
+
: [asText];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case "link":
|
|
134
|
+
return inertLink(node.children || [], node.url || "");
|
|
135
|
+
|
|
136
|
+
case "image":
|
|
137
|
+
return inertLink(
|
|
138
|
+
[textNode(`[image: ${node.alt || ""}]`)],
|
|
139
|
+
node.url || "",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
case "linkReference":
|
|
143
|
+
return inertLink(node.children || [], definitions.get(identifier) || "");
|
|
144
|
+
|
|
145
|
+
case "imageReference":
|
|
146
|
+
return inertLink(
|
|
147
|
+
[textNode(`[image: ${node.alt || ""}]`)],
|
|
148
|
+
definitions.get(identifier) || "",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
return [node];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collectDefinitions(
|
|
157
|
+
node: MarkdownNode,
|
|
158
|
+
into: Map<string, string>,
|
|
159
|
+
): void {
|
|
160
|
+
if (node.type === "definition" && node.identifier) {
|
|
161
|
+
into.set(node.identifier.toLowerCase(), node.url || "");
|
|
162
|
+
}
|
|
163
|
+
for (const child of node.children || []) {
|
|
164
|
+
collectDefinitions(child, into);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Rewrites every link, image and raw HTML node in `text` into inert text, and
|
|
170
|
+
* returns the result as markdown. Safe to call on any string; a parse failure
|
|
171
|
+
* degrades to a fenced code block rather than emitting unneutralized markdown.
|
|
172
|
+
*/
|
|
173
|
+
export default function neutralizeAssistantMarkdown(text: string): string {
|
|
174
|
+
if (!text) {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const tree: MarkdownNode = unified()
|
|
180
|
+
.use(remarkParse)
|
|
181
|
+
.use(remarkGfm)
|
|
182
|
+
.parse(text) as unknown as MarkdownNode;
|
|
183
|
+
|
|
184
|
+
const definitions: Map<string, string> = new Map();
|
|
185
|
+
collectDefinitions(tree, definitions);
|
|
186
|
+
|
|
187
|
+
const children: Array<MarkdownNode> = [];
|
|
188
|
+
for (const child of tree.children || []) {
|
|
189
|
+
children.push(...neutralizeNode(child, definitions, true));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
* A root child demoted to phrasing (a stray inline node left behind once
|
|
194
|
+
* its link wrapper was removed) is not valid at root level; wrap it back
|
|
195
|
+
* into a paragraph so the serializer stays on its typed path.
|
|
196
|
+
*/
|
|
197
|
+
tree.children = children.map((child: MarkdownNode) => {
|
|
198
|
+
return PHRASING_TYPES.has(child.type)
|
|
199
|
+
? { type: "paragraph", children: [child] }
|
|
200
|
+
: child;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return toMarkdown(tree as unknown as MarkdownRoot, {
|
|
204
|
+
extensions: [gfmToMarkdown()],
|
|
205
|
+
bullet: "-",
|
|
206
|
+
}).trim();
|
|
207
|
+
} catch {
|
|
208
|
+
/*
|
|
209
|
+
* Never fall back to returning the raw string: that would ship exactly the
|
|
210
|
+
* live links this function exists to remove. A fence renders the content
|
|
211
|
+
* verbatim and inert.
|
|
212
|
+
*/
|
|
213
|
+
return ["```", text.replace(/```/g, "'''"), "```"].join("\n");
|
|
214
|
+
}
|
|
215
|
+
}
|