@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,316 @@
|
|
|
1
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
2
|
+
import {
|
|
3
|
+
AIChatWidget,
|
|
4
|
+
AIChatWidgetColumn,
|
|
5
|
+
AIChatWidgetSpan,
|
|
6
|
+
AIChatWidgetStat,
|
|
7
|
+
AIChatWidgetType,
|
|
8
|
+
} from "../../../Types/AI/AIChatTypes";
|
|
9
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* The formatting rules the AI chat widgets use on screen, factored out so the
|
|
13
|
+
* markdown and PDF exports render the same numbers, dates and captions the user
|
|
14
|
+
* actually saw. Every helper here mirrors a specific widget component; where a
|
|
15
|
+
* component does something surprising (0 rendering as "0" rather than a dash,
|
|
16
|
+
* a string "1234" not getting thousands separators while the number 1234 does)
|
|
17
|
+
* the behavior is reproduced deliberately rather than tidied up, so an export
|
|
18
|
+
* never disagrees with the chat it came from.
|
|
19
|
+
*
|
|
20
|
+
* These run in the browser, so toLocaleString() and moment resolve against the
|
|
21
|
+
* same locale and timezone the chat rendered in.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Display caps applied by the widget components themselves.
|
|
25
|
+
export const MAX_TABLE_ROWS: number = 25; // DataTableWidget
|
|
26
|
+
export const MAX_ENTITY_ITEMS: number = 12; // EntityListWidget
|
|
27
|
+
export const MAX_SPANS: number = 40; // TraceWaterfallWidget
|
|
28
|
+
|
|
29
|
+
// EntityListWidget.toStr
|
|
30
|
+
export function toDisplayString(value: unknown): string {
|
|
31
|
+
if (value === null || value === undefined) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// EntityListWidget.timeAgo
|
|
38
|
+
export function toTimeAgo(value: unknown): string {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return OneUptimeDate.fromNow(OneUptimeDate.fromString(value as string));
|
|
44
|
+
} catch {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// DataTableWidget.renderCell
|
|
50
|
+
export function formatTableCell(
|
|
51
|
+
value: unknown,
|
|
52
|
+
column: AIChatWidgetColumn,
|
|
53
|
+
): string {
|
|
54
|
+
if (value === null || value === undefined || value === "") {
|
|
55
|
+
return "—";
|
|
56
|
+
}
|
|
57
|
+
if (column.type === "date") {
|
|
58
|
+
try {
|
|
59
|
+
const date: Date = OneUptimeDate.fromString(value as string);
|
|
60
|
+
return `${OneUptimeDate.getDateAsLocalFormattedString(
|
|
61
|
+
date,
|
|
62
|
+
)} · ${OneUptimeDate.fromNow(date)}`;
|
|
63
|
+
} catch {
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (column.type === "number" && typeof value === "number") {
|
|
68
|
+
return value.toLocaleString();
|
|
69
|
+
}
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// StatCardsWidget: the number/string union is discriminated at runtime.
|
|
74
|
+
export function formatStatValue(stat: AIChatWidgetStat): string {
|
|
75
|
+
const value: string =
|
|
76
|
+
typeof stat.value === "number" ? stat.value.toLocaleString() : stat.value;
|
|
77
|
+
return stat.unit ? `${value} ${stat.unit}` : value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ChartWidget.formatValue
|
|
81
|
+
export function formatChartValue(
|
|
82
|
+
value: number,
|
|
83
|
+
unit?: string | undefined,
|
|
84
|
+
): string {
|
|
85
|
+
const rounded: number =
|
|
86
|
+
Math.abs(value) >= 100 ? Math.round(value) : Math.round(value * 100) / 100;
|
|
87
|
+
const text: string = rounded.toLocaleString();
|
|
88
|
+
return unit ? `${text} ${unit}` : text;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ToolApprovalCard.argsPreview
|
|
92
|
+
export function formatToolArguments(args: JSONObject): string {
|
|
93
|
+
const parts: Array<string> = [];
|
|
94
|
+
for (const key of Object.keys(args)) {
|
|
95
|
+
const value: unknown = args[key];
|
|
96
|
+
if (value === undefined || value === null || value === "") {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const valueString: string =
|
|
100
|
+
typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
101
|
+
parts.push(
|
|
102
|
+
`${key}: ${
|
|
103
|
+
valueString.length > 80
|
|
104
|
+
? `${valueString.substring(0, 80)}…`
|
|
105
|
+
: valueString
|
|
106
|
+
}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return parts.join(" · ");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* TraceWaterfallWidget: depth is the length of the parent chain, guarded
|
|
114
|
+
* against cycles and runaway nesting exactly as the component guards it.
|
|
115
|
+
*/
|
|
116
|
+
export function computeSpanDepth(
|
|
117
|
+
span: AIChatWidgetSpan,
|
|
118
|
+
parentBySpanId: Map<string, string | undefined>,
|
|
119
|
+
): number {
|
|
120
|
+
let depth: number = 0;
|
|
121
|
+
let current: string | undefined = span.parentSpanId;
|
|
122
|
+
const seen: Set<string> = new Set();
|
|
123
|
+
while (current && parentBySpanId.has(current) && !seen.has(current)) {
|
|
124
|
+
seen.add(current);
|
|
125
|
+
depth++;
|
|
126
|
+
current = parentBySpanId.get(current);
|
|
127
|
+
if (depth > 12) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return depth;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function buildParentBySpanId(
|
|
135
|
+
spans: Array<AIChatWidgetSpan>,
|
|
136
|
+
): Map<string, string | undefined> {
|
|
137
|
+
const parentBySpanId: Map<string, string | undefined> = new Map();
|
|
138
|
+
for (const span of spans) {
|
|
139
|
+
parentBySpanId.set(span.spanId, span.parentSpanId);
|
|
140
|
+
}
|
|
141
|
+
return parentBySpanId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface SpanBarMetrics {
|
|
145
|
+
leftPercent: number;
|
|
146
|
+
widthPercent: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// TraceWaterfallWidget bar geometry, including its clamps.
|
|
150
|
+
export function computeSpanBar(
|
|
151
|
+
span: AIChatWidgetSpan,
|
|
152
|
+
totalDurationMs: number,
|
|
153
|
+
): SpanBarMetrics {
|
|
154
|
+
const leftPercent: number = Math.min(
|
|
155
|
+
99,
|
|
156
|
+
(span.startOffsetMs / totalDurationMs) * 100,
|
|
157
|
+
);
|
|
158
|
+
const widthPercent: number = Math.max(
|
|
159
|
+
1.5,
|
|
160
|
+
Math.min(100 - leftPercent, (span.durationMs / totalDurationMs) * 100),
|
|
161
|
+
);
|
|
162
|
+
return { leftPercent: leftPercent, widthPercent: widthPercent };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// TraceWaterfallWidget floors the denominator so the division is always safe.
|
|
166
|
+
export function totalTraceDuration(widget: AIChatWidget): number {
|
|
167
|
+
return Math.max(widget.data.totalDurationMs || 0, 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
* The fields EntityListWidget actually reads off an item, per list type. The
|
|
172
|
+
* component ignores everything else on the item, so the export does too.
|
|
173
|
+
*/
|
|
174
|
+
export interface EntityListEntry {
|
|
175
|
+
heading: string;
|
|
176
|
+
body: string;
|
|
177
|
+
badges: Array<string>;
|
|
178
|
+
footer: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function toEntityListEntry(
|
|
182
|
+
item: JSONObject,
|
|
183
|
+
widgetType: AIChatWidgetType,
|
|
184
|
+
): EntityListEntry {
|
|
185
|
+
if (widgetType === AIChatWidgetType.ExceptionList) {
|
|
186
|
+
const badges: Array<string> = [
|
|
187
|
+
`${toDisplayString(item["occurrences"]) || "0"}×`,
|
|
188
|
+
];
|
|
189
|
+
if (item["isResolved"]) {
|
|
190
|
+
badges.push("Resolved");
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
heading: toDisplayString(item["type"]) || "Exception",
|
|
194
|
+
body: toDisplayString(item["message"]),
|
|
195
|
+
badges: badges,
|
|
196
|
+
footer: `Last seen ${toTimeAgo(item["lastSeenAt"])}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const numberField: string =
|
|
201
|
+
widgetType === AIChatWidgetType.AlertList
|
|
202
|
+
? toDisplayString(item["alertNumber"])
|
|
203
|
+
: toDisplayString(item["incidentNumber"]);
|
|
204
|
+
|
|
205
|
+
const badges: Array<string> = [];
|
|
206
|
+
if (item["state"]) {
|
|
207
|
+
badges.push(toDisplayString(item["state"]));
|
|
208
|
+
}
|
|
209
|
+
if (item["severity"]) {
|
|
210
|
+
badges.push(toDisplayString(item["severity"]));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
heading: `${numberField ? `#${numberField} · ` : ""}${toDisplayString(
|
|
215
|
+
item["title"],
|
|
216
|
+
)}`,
|
|
217
|
+
body: item["description"] ? toDisplayString(item["description"]) : "",
|
|
218
|
+
badges: badges,
|
|
219
|
+
footer: item["createdAt"] ? `Created ${toTimeAgo(item["createdAt"])}` : "",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function isEntityListWidget(type: AIChatWidgetType): boolean {
|
|
224
|
+
return (
|
|
225
|
+
type === AIChatWidgetType.IncidentList ||
|
|
226
|
+
type === AIChatWidgetType.AlertList ||
|
|
227
|
+
type === AIChatWidgetType.ExceptionList
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function isChartWidget(type: AIChatWidgetType): boolean {
|
|
232
|
+
return (
|
|
233
|
+
type === AIChatWidgetType.TimeSeriesChart ||
|
|
234
|
+
type === AIChatWidgetType.BarChart
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/*
|
|
239
|
+
* A chart point resolved for export. `x` keeps the raw value so the exporter
|
|
240
|
+
* can label a category axis, while `date` is set only when the value actually
|
|
241
|
+
* parses as a time.
|
|
242
|
+
*/
|
|
243
|
+
export interface ExportChartPoint {
|
|
244
|
+
x: string;
|
|
245
|
+
date: Date | undefined;
|
|
246
|
+
y: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface ExportChartSeries {
|
|
250
|
+
name: string;
|
|
251
|
+
points: Array<ExportChartPoint>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
* Resolves a chart widget's series for export.
|
|
256
|
+
*
|
|
257
|
+
* Note this deliberately keeps the RAW points rather than reproducing the
|
|
258
|
+
* on-screen bucketing. The chart component re-buckets points into intervals to
|
|
259
|
+
* fit Recharts, and that pass is lossy in ways that are fine for a glanceable
|
|
260
|
+
* chart but not for a downloaded record: it matches points to buckets by
|
|
261
|
+
* formatted-label string equality and silently drops the misses, so a weekly
|
|
262
|
+
* chart keeps roughly one point in seven and a monthly one can skip a month
|
|
263
|
+
* outright. An export is the artifact people keep and audit, so it plots what
|
|
264
|
+
* the tools actually returned. For the common case — the server already emits
|
|
265
|
+
* one point per bucket — the two agree exactly.
|
|
266
|
+
*/
|
|
267
|
+
export function toExportChartSeries(
|
|
268
|
+
widget: AIChatWidget,
|
|
269
|
+
): Array<ExportChartSeries> {
|
|
270
|
+
const series: Array<ExportChartSeries> = [];
|
|
271
|
+
|
|
272
|
+
for (const raw of widget.data.series || []) {
|
|
273
|
+
const points: Array<ExportChartPoint> = [];
|
|
274
|
+
for (const point of raw.points || []) {
|
|
275
|
+
if (point.y === null || point.y === undefined) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
let date: Date | undefined = undefined;
|
|
279
|
+
try {
|
|
280
|
+
const parsed: Date = OneUptimeDate.fromString(point.x);
|
|
281
|
+
if (!isNaN(parsed.getTime())) {
|
|
282
|
+
date = parsed;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
date = undefined;
|
|
286
|
+
}
|
|
287
|
+
points.push({ x: point.x, date: date, y: point.y });
|
|
288
|
+
}
|
|
289
|
+
series.push({ name: raw.name, points: points });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return series;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function chartHasData(series: Array<ExportChartSeries>): boolean {
|
|
296
|
+
return series.some((item: ExportChartSeries) => {
|
|
297
|
+
return item.points.length > 0;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
* True when every plotted point carries a real timestamp, which is what lets
|
|
303
|
+
* the exporter lay out a proportional time axis instead of even categories.
|
|
304
|
+
*/
|
|
305
|
+
export function chartIsTimeBased(series: Array<ExportChartSeries>): boolean {
|
|
306
|
+
let seen: boolean = false;
|
|
307
|
+
for (const item of series) {
|
|
308
|
+
for (const point of item.points) {
|
|
309
|
+
if (!point.date) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
seen = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return seen;
|
|
316
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AIChatCitation,
|
|
3
|
+
AIChatToolAction,
|
|
4
|
+
AIChatWidget,
|
|
5
|
+
AIChatWidgetColumn,
|
|
6
|
+
AIChatWidgetSpan,
|
|
7
|
+
AIChatWidgetStat,
|
|
8
|
+
AIChatWidgetType,
|
|
9
|
+
} from "../../../Types/AI/AIChatTypes";
|
|
10
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
11
|
+
import {
|
|
12
|
+
ExportChartPoint,
|
|
13
|
+
ExportChartSeries,
|
|
14
|
+
EntityListEntry,
|
|
15
|
+
MAX_ENTITY_ITEMS,
|
|
16
|
+
MAX_SPANS,
|
|
17
|
+
MAX_TABLE_ROWS,
|
|
18
|
+
buildParentBySpanId,
|
|
19
|
+
chartHasData,
|
|
20
|
+
computeSpanDepth,
|
|
21
|
+
formatChartValue,
|
|
22
|
+
formatStatValue,
|
|
23
|
+
formatTableCell,
|
|
24
|
+
formatToolArguments,
|
|
25
|
+
isChartWidget,
|
|
26
|
+
isEntityListWidget,
|
|
27
|
+
toEntityListEntry,
|
|
28
|
+
toExportChartSeries,
|
|
29
|
+
totalTraceDuration,
|
|
30
|
+
} from "./ChatWidgetData";
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Renders the inline artifacts of an assistant message — widgets, tool actions
|
|
34
|
+
* and citations — as plain markdown. A widget is a picture of data, and the
|
|
35
|
+
* data is what survives the trip to a text file, so each one becomes the
|
|
36
|
+
* clearest text shape for its type: charts and waterfalls become tables,
|
|
37
|
+
* entity lists become lists, and stat cards become key/value rows.
|
|
38
|
+
*
|
|
39
|
+
* Values are formatted through ChatWidgetData so the text matches the chat.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// A cell is escaped so a stray pipe or newline cannot break the table shape.
|
|
43
|
+
function escapeTableCell(value: string): string {
|
|
44
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function markdownTable(
|
|
48
|
+
headers: Array<string>,
|
|
49
|
+
alignments: Array<"left" | "right">,
|
|
50
|
+
rows: Array<Array<string>>,
|
|
51
|
+
): Array<string> {
|
|
52
|
+
const divider: Array<string> = alignments.map((align: "left" | "right") => {
|
|
53
|
+
return align === "right" ? "---:" : "---";
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
`| ${headers.map(escapeTableCell).join(" | ")} |`,
|
|
58
|
+
`| ${divider.join(" | ")} |`,
|
|
59
|
+
...rows.map((row: Array<string>) => {
|
|
60
|
+
return `| ${row.map(escapeTableCell).join(" | ")} |`;
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function chartToMarkdown(widget: AIChatWidget): Array<string> {
|
|
66
|
+
const series: Array<ExportChartSeries> = toExportChartSeries(widget);
|
|
67
|
+
|
|
68
|
+
if (!chartHasData(series)) {
|
|
69
|
+
return ["_No data points in this range._"];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const kind: string =
|
|
73
|
+
widget.type === AIChatWidgetType.BarChart ? "Bar chart" : "Line chart";
|
|
74
|
+
const lines: Array<string> = [`_${kind}_`, ""];
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* One table per series rather than a single wide table keyed by x: series
|
|
78
|
+
* rarely share x values exactly, and a joined table would be mostly holes.
|
|
79
|
+
*/
|
|
80
|
+
for (const item of series) {
|
|
81
|
+
if (series.length > 1) {
|
|
82
|
+
lines.push(`**${item.name}**`, "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (item.points.length === 0) {
|
|
86
|
+
lines.push("_No data points._", "");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lines.push(
|
|
91
|
+
...markdownTable(
|
|
92
|
+
[
|
|
93
|
+
widget.data.xIsTime === false ? "Category" : "Time",
|
|
94
|
+
item.name || "Value",
|
|
95
|
+
],
|
|
96
|
+
["left", "right"],
|
|
97
|
+
item.points.map((point: ExportChartPoint) => {
|
|
98
|
+
return [
|
|
99
|
+
point.date ? point.date.toLocaleString() : point.x,
|
|
100
|
+
formatChartValue(point.y, widget.data.unit),
|
|
101
|
+
];
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
"",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function tableToMarkdown(widget: AIChatWidget): Array<string> {
|
|
112
|
+
const columns: Array<AIChatWidgetColumn> = widget.data.columns || [];
|
|
113
|
+
const allRows: Array<JSONObject> = widget.data.rows || [];
|
|
114
|
+
const rows: Array<JSONObject> = allRows.slice(0, MAX_TABLE_ROWS);
|
|
115
|
+
|
|
116
|
+
if (columns.length === 0 || rows.length === 0) {
|
|
117
|
+
return ["_No rows._"];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines: Array<string> = markdownTable(
|
|
121
|
+
columns.map((column: AIChatWidgetColumn) => {
|
|
122
|
+
return column.title;
|
|
123
|
+
}),
|
|
124
|
+
columns.map((column: AIChatWidgetColumn) => {
|
|
125
|
+
return column.type === "number" ? "right" : "left";
|
|
126
|
+
}),
|
|
127
|
+
rows.map((row: JSONObject) => {
|
|
128
|
+
return columns.map((column: AIChatWidgetColumn) => {
|
|
129
|
+
return formatTableCell(row[column.key], column);
|
|
130
|
+
});
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (allRows.length > rows.length) {
|
|
135
|
+
lines.push("", `_Showing ${rows.length} of ${allRows.length} rows._`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function waterfallToMarkdown(widget: AIChatWidget): Array<string> {
|
|
142
|
+
const spans: Array<AIChatWidgetSpan> = widget.data.spans || [];
|
|
143
|
+
const shown: Array<AIChatWidgetSpan> = spans.slice(0, MAX_SPANS);
|
|
144
|
+
|
|
145
|
+
if (shown.length === 0) {
|
|
146
|
+
return ["_No spans._"];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const parentBySpanId: Map<string, string | undefined> =
|
|
150
|
+
buildParentBySpanId(spans);
|
|
151
|
+
const totalDurationMs: number = totalTraceDuration(widget);
|
|
152
|
+
|
|
153
|
+
const lines: Array<string> = markdownTable(
|
|
154
|
+
["Span", "Start", "Duration"],
|
|
155
|
+
["left", "right", "right"],
|
|
156
|
+
shown.map((span: AIChatWidgetSpan) => {
|
|
157
|
+
const depth: number = computeSpanDepth(span, parentBySpanId);
|
|
158
|
+
// Nesting is the point of a waterfall, so keep it as leading indent.
|
|
159
|
+
const indent: string = " ".repeat(Math.min(depth, 8));
|
|
160
|
+
return [
|
|
161
|
+
`${indent}${span.isError ? "● " : ""}${span.name}`,
|
|
162
|
+
`${span.startOffsetMs.toLocaleString()} ms`,
|
|
163
|
+
`${span.durationMs.toLocaleString()} ms`,
|
|
164
|
+
];
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
lines.push(
|
|
169
|
+
"",
|
|
170
|
+
`_Total trace duration: ${totalDurationMs.toLocaleString()} ms._`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (spans.length > shown.length) {
|
|
174
|
+
lines.push(`_Showing ${shown.length} of ${spans.length} spans._`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return lines;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function entityListToMarkdown(widget: AIChatWidget): Array<string> {
|
|
181
|
+
const items: Array<JSONObject> = widget.data.items || [];
|
|
182
|
+
const shown: Array<JSONObject> = items.slice(0, MAX_ENTITY_ITEMS);
|
|
183
|
+
|
|
184
|
+
if (shown.length === 0) {
|
|
185
|
+
return ["_Nothing here._"];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const lines: Array<string> = [];
|
|
189
|
+
|
|
190
|
+
for (const item of shown) {
|
|
191
|
+
const entry: EntityListEntry = toEntityListEntry(item, widget.type);
|
|
192
|
+
const badges: string = entry.badges.length
|
|
193
|
+
? ` — ${entry.badges.join(" · ")}`
|
|
194
|
+
: "";
|
|
195
|
+
lines.push(`- **${entry.heading}**${badges}`);
|
|
196
|
+
if (entry.body) {
|
|
197
|
+
lines.push(` - ${entry.body}`);
|
|
198
|
+
}
|
|
199
|
+
if (entry.footer) {
|
|
200
|
+
lines.push(` - _${entry.footer}_`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (items.length > shown.length) {
|
|
205
|
+
lines.push("", `_Showing ${shown.length} of ${items.length}._`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return lines;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function statCardsToMarkdown(widget: AIChatWidget): Array<string> {
|
|
212
|
+
const stats: Array<AIChatWidgetStat> = widget.data.stats || [];
|
|
213
|
+
|
|
214
|
+
if (stats.length === 0) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return markdownTable(
|
|
219
|
+
["Metric", "Value"],
|
|
220
|
+
["left", "right"],
|
|
221
|
+
stats.map((stat: AIChatWidgetStat) => {
|
|
222
|
+
return [stat.label, formatStatValue(stat)];
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resourceCardToMarkdown(widget: AIChatWidget): Array<string> {
|
|
228
|
+
const lines: Array<string> = [];
|
|
229
|
+
|
|
230
|
+
if (widget.data.resourceType) {
|
|
231
|
+
lines.push(`_${widget.data.resourceType.toUpperCase()}_`, "");
|
|
232
|
+
}
|
|
233
|
+
if (widget.data.heading) {
|
|
234
|
+
lines.push(`**${widget.data.heading}**`);
|
|
235
|
+
}
|
|
236
|
+
if (widget.data.subheading) {
|
|
237
|
+
lines.push(widget.data.subheading);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const fields: Array<{ label: string; value: string }> =
|
|
241
|
+
widget.data.fields || [];
|
|
242
|
+
|
|
243
|
+
if (fields.length > 0) {
|
|
244
|
+
lines.push("");
|
|
245
|
+
for (const field of fields) {
|
|
246
|
+
lines.push(`- **${field.label}:** ${field.value}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return lines;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function widgetBodyToMarkdown(widget: AIChatWidget): Array<string> {
|
|
254
|
+
if (isChartWidget(widget.type)) {
|
|
255
|
+
return chartToMarkdown(widget);
|
|
256
|
+
}
|
|
257
|
+
if (isEntityListWidget(widget.type)) {
|
|
258
|
+
return entityListToMarkdown(widget);
|
|
259
|
+
}
|
|
260
|
+
switch (widget.type) {
|
|
261
|
+
case AIChatWidgetType.Table:
|
|
262
|
+
return tableToMarkdown(widget);
|
|
263
|
+
case AIChatWidgetType.TraceWaterfall:
|
|
264
|
+
return waterfallToMarkdown(widget);
|
|
265
|
+
case AIChatWidgetType.StatCards:
|
|
266
|
+
return statCardsToMarkdown(widget);
|
|
267
|
+
case AIChatWidgetType.ResourceCard:
|
|
268
|
+
return resourceCardToMarkdown(widget);
|
|
269
|
+
default:
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function widgetToMarkdown(widget: AIChatWidget): string {
|
|
275
|
+
const lines: Array<string> = [`#### ${widget.title}`, ""];
|
|
276
|
+
|
|
277
|
+
if (widget.description) {
|
|
278
|
+
lines.push(`_${widget.description}_`, "");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
lines.push(...widgetBodyToMarkdown(widget));
|
|
282
|
+
|
|
283
|
+
return lines.join("\n").trim();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function widgetsToMarkdown(widgets: Array<AIChatWidget>): string {
|
|
287
|
+
return widgets
|
|
288
|
+
.map((widget: AIChatWidget) => {
|
|
289
|
+
return widgetToMarkdown(widget);
|
|
290
|
+
})
|
|
291
|
+
.join("\n\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function toolActionsToMarkdown(
|
|
295
|
+
toolActions: Array<AIChatToolAction>,
|
|
296
|
+
): string {
|
|
297
|
+
if (toolActions.length === 0) {
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const lines: Array<string> = ["#### Actions", ""];
|
|
302
|
+
|
|
303
|
+
for (const action of toolActions) {
|
|
304
|
+
lines.push(`- **${action.status}** — ${action.title}`);
|
|
305
|
+
if (action.description) {
|
|
306
|
+
lines.push(` - ${action.description}`);
|
|
307
|
+
}
|
|
308
|
+
const args: string = formatToolArguments(action.arguments || {});
|
|
309
|
+
if (args) {
|
|
310
|
+
lines.push(` - \`${args.replace(/`/g, "'")}\``);
|
|
311
|
+
}
|
|
312
|
+
if (action.resultSummary) {
|
|
313
|
+
lines.push(` - _${action.resultSummary}_`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return lines.join("\n");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/*
|
|
321
|
+
* Citations carry the query that produced an answer, which is the whole point
|
|
322
|
+
* of exporting them: a reader can see what was actually asked of the data. A
|
|
323
|
+
* rowCount of 0 is meaningful evidence ("checked, found nothing"), so it is
|
|
324
|
+
* spelled out rather than shown as a bare zero.
|
|
325
|
+
*/
|
|
326
|
+
export function citationsToMarkdown(citations: Array<AIChatCitation>): string {
|
|
327
|
+
if (citations.length === 0) {
|
|
328
|
+
return "";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const lines: Array<string> = ["#### Sources", ""];
|
|
332
|
+
|
|
333
|
+
for (const citation of citations) {
|
|
334
|
+
const rows: string =
|
|
335
|
+
citation.rowCount === 0
|
|
336
|
+
? "checked, found nothing"
|
|
337
|
+
: `${citation.rowCount} ${citation.rowCount === 1 ? "row" : "rows"}`;
|
|
338
|
+
lines.push(`- **${citation.id}** ${citation.label} — ${rows}`);
|
|
339
|
+
lines.push(` - Tool: \`${citation.toolName}\``);
|
|
340
|
+
lines.push(
|
|
341
|
+
` - Query: \`${JSON.stringify(citation.queryArguments || {}).replace(
|
|
342
|
+
/`/g,
|
|
343
|
+
"'",
|
|
344
|
+
)}\``,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return lines.join("\n");
|
|
349
|
+
}
|