@oneuptime/common 11.3.25 → 11.3.26

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.
Files changed (189) hide show
  1. package/Models/DatabaseModels/AIConversation.ts +322 -0
  2. package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
  3. package/Models/DatabaseModels/AIRun.ts +584 -0
  4. package/Models/DatabaseModels/AIRunEvent.ts +443 -0
  5. package/Models/DatabaseModels/Index.ts +8 -0
  6. package/Models/DatabaseModels/LlmLog.ts +26 -0
  7. package/Models/DatabaseModels/LlmProvider.ts +2 -2
  8. package/Server/API/AIChatAPI.ts +693 -0
  9. package/Server/API/LlmProviderAPI.ts +169 -0
  10. package/Server/API/MicrosoftTeamsAPI.ts +5 -0
  11. package/Server/API/SlackAPI.ts +663 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  17. package/Server/Services/AIConversationMessageService.ts +39 -0
  18. package/Server/Services/AIConversationService.ts +63 -0
  19. package/Server/Services/AIRunEventService.ts +39 -0
  20. package/Server/Services/AIRunService.ts +39 -0
  21. package/Server/Services/AIService.ts +53 -31
  22. package/Server/Services/DatabaseService.ts +19 -0
  23. package/Server/Services/LlmProviderService.ts +110 -0
  24. package/Server/Services/ProjectService.ts +24 -0
  25. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
  26. package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
  27. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1054 -0
  28. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
  29. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
  30. package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
  31. package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
  32. package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
  33. package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
  34. package/Server/Utils/AI/Toolbox/IncidentTools.ts +204 -0
  35. package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
  36. package/Server/Utils/AI/Toolbox/Index.ts +228 -0
  37. package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
  38. package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
  39. package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
  40. package/Server/Utils/AI/Toolbox/Serializer.ts +257 -0
  41. package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
  42. package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
  43. package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
  44. package/Server/Utils/LLM/LLMService.ts +502 -92
  45. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
  46. package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
  47. package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  48. package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
  49. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
  50. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
  51. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +308 -0
  52. package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
  53. package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
  54. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +155 -0
  55. package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
  56. package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
  57. package/Types/AI/AIChatMessageRole.ts +6 -0
  58. package/Types/AI/AIChatMessageStatus.ts +32 -0
  59. package/Types/AI/AIChatPermissionMode.ts +69 -0
  60. package/Types/AI/AIChatTypes.ts +231 -0
  61. package/Types/AI/AIRunEventType.ts +17 -0
  62. package/Types/AI/AIRunStatus.ts +28 -0
  63. package/Types/AI/AIRunType.ts +6 -0
  64. package/Types/LLM/LlmType.ts +6 -0
  65. package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
  66. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
  67. package/UI/Components/Page/Page.tsx +14 -0
  68. package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
  69. package/UI/Utils/TestLLMProvider.ts +59 -0
  70. package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
  71. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
  72. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
  73. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
  74. package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
  75. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
  76. package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
  77. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
  78. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  79. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  80. package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
  81. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
  82. package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
  83. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  84. package/build/dist/Server/API/AIChatAPI.js +498 -0
  85. package/build/dist/Server/API/AIChatAPI.js.map +1 -0
  86. package/build/dist/Server/API/LlmProviderAPI.js +107 -1
  87. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  88. package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
  89. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  90. package/build/dist/Server/API/SlackAPI.js +442 -0
  91. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
  94. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  101. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  102. package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
  103. package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
  104. package/build/dist/Server/Services/AIConversationService.js +42 -0
  105. package/build/dist/Server/Services/AIConversationService.js.map +1 -0
  106. package/build/dist/Server/Services/AIRunEventService.js +34 -0
  107. package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
  108. package/build/dist/Server/Services/AIRunService.js +34 -0
  109. package/build/dist/Server/Services/AIRunService.js.map +1 -0
  110. package/build/dist/Server/Services/AIService.js +31 -23
  111. package/build/dist/Server/Services/AIService.js.map +1 -1
  112. package/build/dist/Server/Services/DatabaseService.js +10 -0
  113. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/LlmProviderService.js +108 -0
  115. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  116. package/build/dist/Server/Services/ProjectService.js +25 -0
  117. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  118. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
  119. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  120. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
  121. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
  122. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +745 -0
  123. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
  124. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
  125. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
  126. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
  127. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
  128. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
  129. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
  130. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
  131. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
  132. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
  133. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
  134. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
  135. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
  136. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +167 -0
  137. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
  138. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
  139. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
  140. package/build/dist/Server/Utils/AI/Toolbox/Index.js +150 -0
  141. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
  142. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
  143. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
  144. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
  145. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
  146. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
  147. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
  148. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +188 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
  150. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
  151. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
  152. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
  153. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
  154. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
  155. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
  156. package/build/dist/Server/Utils/LLM/LLMService.js +372 -88
  157. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  158. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  160. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
  161. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  162. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  163. package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
  164. package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
  165. package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
  166. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
  167. package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
  168. package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
  169. package/build/dist/Types/AI/AIChatTypes.js +74 -0
  170. package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
  171. package/build/dist/Types/AI/AIRunEventType.js +18 -0
  172. package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
  173. package/build/dist/Types/AI/AIRunStatus.js +26 -0
  174. package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
  175. package/build/dist/Types/AI/AIRunType.js +7 -0
  176. package/build/dist/Types/AI/AIRunType.js.map +1 -0
  177. package/build/dist/Types/LLM/LlmType.js +6 -0
  178. package/build/dist/Types/LLM/LlmType.js.map +1 -1
  179. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
  180. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  181. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
  182. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  183. package/build/dist/UI/Components/Page/Page.js +3 -1
  184. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  185. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
  186. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
  187. package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
  188. package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
  189. package/package.json +1 -1
@@ -0,0 +1,402 @@
1
+ import Span from "../../../../Models/AnalyticsModels/Span";
2
+ import DatabaseRequestType from "../../../Types/BaseDatabase/DatabaseRequestType";
3
+ import ModelPermission from "../../../Types/AnalyticsDatabase/ModelPermission";
4
+ import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
5
+ import BadDataException from "../../../../Types/Exception/BadDataException";
6
+ import { JSONObject } from "../../../../Types/JSON";
7
+ import ObjectID from "../../../../Types/ObjectID";
8
+ import Permission from "../../../../Types/Permission";
9
+ import {
10
+ AIChatCitationTargetType,
11
+ AIChatWidgetSpan,
12
+ } from "../../../../Types/AI/AIChatTypes";
13
+ import SpanService from "../../../Services/SpanService";
14
+ import TraceAggregationService, {
15
+ TraceAnalyticsTableRow,
16
+ } from "../../../Services/TraceAggregationService";
17
+ import ToolResultSerializer, { SerializedResult } from "./Serializer";
18
+ import WidgetBuilder from "./WidgetBuilder";
19
+ import {
20
+ ObservabilityTool,
21
+ TimeRangeSchemaProperties,
22
+ ToolArgs,
23
+ ToolContext,
24
+ ToolExecutionResult,
25
+ } from "./ToolTypes";
26
+
27
+ const TRACE_READ_PERMISSIONS: Array<Permission> = [
28
+ Permission.ProjectOwner,
29
+ Permission.ProjectAdmin,
30
+ Permission.ProjectMember,
31
+ Permission.Viewer,
32
+ Permission.TelemetryAdmin,
33
+ Permission.TelemetryMember,
34
+ Permission.TelemetryViewer,
35
+ Permission.ReadTelemetryServiceTraces,
36
+ ];
37
+
38
+ const VALID_METRICS: Array<string> = [
39
+ "count",
40
+ "errorCount",
41
+ "avgDuration",
42
+ "minDuration",
43
+ "maxDuration",
44
+ "p50Duration",
45
+ "p90Duration",
46
+ "p95Duration",
47
+ "p99Duration",
48
+ ];
49
+
50
+ const VALID_GROUP_BY: Array<string> = [
51
+ "name",
52
+ "primaryEntityId",
53
+ "kind",
54
+ "statusCode",
55
+ "isRootSpan",
56
+ ];
57
+
58
+ /*
59
+ * Upper bound on spans fetched for a single trace waterfall. High enough for
60
+ * almost every real trace; when a trace exceeds it we say so explicitly rather
61
+ * than silently dropping spans (which also orphans their children into fake
62
+ * roots).
63
+ */
64
+ const MAX_TRACE_SPANS: number = 500;
65
+
66
+ export const QueryTracesTool: ObservabilityTool = {
67
+ name: "query_traces",
68
+ description:
69
+ "Analyze spans/traces: latency profile (count, errorCount, avg and p50/p90/p95/p99 durations in ms) grouped by one dimension such as span name or service. Use this to answer 'why is X slow', 'error rate by operation' or 'slowest endpoints'. Then drill into a single trace with get_trace.",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ ...TimeRangeSchemaProperties,
74
+ groupBy: {
75
+ type: "string",
76
+ enum: ["name", "primaryEntityId", "kind", "statusCode", "isRootSpan"],
77
+ description:
78
+ "Dimension to group by (default name = span/operation name; primaryEntityId = service).",
79
+ },
80
+ metric: {
81
+ type: "string",
82
+ enum: [
83
+ "count",
84
+ "errorCount",
85
+ "avgDuration",
86
+ "minDuration",
87
+ "maxDuration",
88
+ "p50Duration",
89
+ "p90Duration",
90
+ "p95Duration",
91
+ "p99Duration",
92
+ ],
93
+ description:
94
+ "Metric used to rank the groups (default p95Duration). The full latency profile is always returned per group.",
95
+ },
96
+ nameSearchText: {
97
+ type: "string",
98
+ description: "Only spans whose name contains this text.",
99
+ },
100
+ serviceId: {
101
+ type: "string",
102
+ description: "Only spans from this telemetry service.",
103
+ },
104
+ hasException: {
105
+ type: "boolean",
106
+ description: "Only spans that recorded an exception.",
107
+ },
108
+ rootOnly: {
109
+ type: "boolean",
110
+ description: "Only root spans (whole-request latency).",
111
+ },
112
+ limit: {
113
+ type: "number",
114
+ description: "Maximum groups to return (default 10, max 25).",
115
+ },
116
+ },
117
+ },
118
+ requiredPermissions: TRACE_READ_PERMISSIONS,
119
+ execute: async (
120
+ args: JSONObject,
121
+ ctx: ToolContext,
122
+ ): Promise<ToolExecutionResult> => {
123
+ const { startTime, endTime } = ToolArgs.getTimeRange(args, {
124
+ defaultHours: 24,
125
+ maxDays: 30,
126
+ });
127
+
128
+ const metric: string = ToolArgs.getString(args, "metric") || "p95Duration";
129
+ if (!VALID_METRICS.includes(metric)) {
130
+ throw new BadDataException(`Invalid metric: ${metric}`);
131
+ }
132
+
133
+ const groupBy: string = ToolArgs.getString(args, "groupBy") || "name";
134
+ if (!VALID_GROUP_BY.includes(groupBy)) {
135
+ throw new BadDataException(`Invalid groupBy: ${groupBy}`);
136
+ }
137
+
138
+ const limit: number = ToolArgs.getNumber(args, "limit", {
139
+ defaultValue: 10,
140
+ min: 1,
141
+ max: 25,
142
+ });
143
+
144
+ const serviceId: ObjectID | undefined = ToolArgs.getObjectID(
145
+ args,
146
+ "serviceId",
147
+ );
148
+
149
+ const windowMinutes: number =
150
+ (endTime.getTime() - startTime.getTime()) / (60 * 1000);
151
+
152
+ /*
153
+ * getAnalyticsTable builds raw aggregation SQL and skips the model layer's
154
+ * owned-scope filter, so a label-restricted user would otherwise see
155
+ * project-wide trace analytics. Constrain to the services this user may
156
+ * read (spans are owned through Service, same as logs).
157
+ */
158
+ const accessibleServiceIds: Array<ObjectID> | null =
159
+ await ModelPermission.getAccessibleServiceIdsForAnalyticsModel(
160
+ Span,
161
+ ctx.props,
162
+ DatabaseRequestType.Read,
163
+ );
164
+
165
+ const tableRows: Array<TraceAnalyticsTableRow> =
166
+ await TraceAggregationService.getAnalyticsTable({
167
+ projectId: ctx.projectId,
168
+ startTime: startTime,
169
+ endTime: endTime,
170
+ bucketSizeInMinutes: Math.max(1, Math.round(windowMinutes / 60)),
171
+ chartType: "table",
172
+ metric: metric as never,
173
+ groupBy: [groupBy],
174
+ limit: limit,
175
+ nameSearchText: ToolArgs.getString(args, "nameSearchText"),
176
+ serviceIds: ToolArgs.scopeServiceIds(accessibleServiceIds, serviceId),
177
+ hasException: ToolArgs.getBoolean(args, "hasException"),
178
+ rootOnly: ToolArgs.getBoolean(args, "rootOnly"),
179
+ });
180
+
181
+ const rows: Array<JSONObject> = tableRows.map(
182
+ (row: TraceAnalyticsTableRow) => {
183
+ return {
184
+ [groupBy]: Object.values(row.groupValues).join(" / "),
185
+ count: row.count,
186
+ errorCount: row.errorCount,
187
+ avgMs: Math.round(row.avgDurationMs * 100) / 100,
188
+ p50Ms: Math.round(row.p50DurationMs * 100) / 100,
189
+ p90Ms: Math.round(row.p90DurationMs * 100) / 100,
190
+ p95Ms: Math.round(row.p95DurationMs * 100) / 100,
191
+ p99Ms: Math.round(row.p99DurationMs * 100) / 100,
192
+ };
193
+ },
194
+ );
195
+
196
+ const serialized: SerializedResult =
197
+ ToolResultSerializer.serializeRows(rows);
198
+
199
+ return {
200
+ dataForLlm: serialized.text,
201
+ rowCount: serialized.rowCount,
202
+ citationLabel: `Trace analytics by ${groupBy} (${metric}), ${startTime.toISOString()} – ${endTime.toISOString()}`,
203
+ citationTarget: {
204
+ type: AIChatCitationTargetType.Traces,
205
+ },
206
+ redactionCount: serialized.redactionCount,
207
+ isTruncated: serialized.isTruncated,
208
+ widget:
209
+ rows.length > 0
210
+ ? WidgetBuilder.table({
211
+ title: `Trace analytics by ${groupBy}`,
212
+ description: `Ranked by ${metric} · ${startTime.toISOString()} – ${endTime.toISOString()}`,
213
+ columns: [
214
+ { key: groupBy, title: groupBy, type: "text" },
215
+ { key: "count", title: "Count", type: "number" },
216
+ { key: "errorCount", title: "Errors", type: "number" },
217
+ { key: "avgMs", title: "Avg (ms)", type: "number" },
218
+ { key: "p90Ms", title: "p90 (ms)", type: "number" },
219
+ { key: "p95Ms", title: "p95 (ms)", type: "number" },
220
+ { key: "p99Ms", title: "p99 (ms)", type: "number" },
221
+ ],
222
+ rows: rows,
223
+ link: { type: AIChatCitationTargetType.Traces },
224
+ })
225
+ : undefined,
226
+ };
227
+ },
228
+ };
229
+
230
+ interface SpanTreeNode {
231
+ span: Span;
232
+ children: Array<SpanTreeNode>;
233
+ }
234
+
235
+ export const GetTraceTool: ObservabilityTool = {
236
+ name: "get_trace",
237
+ description:
238
+ "Get the span tree (waterfall) of one trace by traceId: every span with its duration, status and whether it recorded an exception. Use after query_traces or search_logs surfaces an interesting traceId.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ traceId: {
243
+ type: "string",
244
+ description: "The trace ID (required).",
245
+ },
246
+ },
247
+ required: ["traceId"],
248
+ },
249
+ requiredPermissions: TRACE_READ_PERMISSIONS,
250
+ execute: async (
251
+ args: JSONObject,
252
+ ctx: ToolContext,
253
+ ): Promise<ToolExecutionResult> => {
254
+ const traceId: string | undefined = ToolArgs.getString(args, "traceId");
255
+
256
+ if (!traceId) {
257
+ throw new BadDataException("traceId is required.");
258
+ }
259
+
260
+ const spans: Array<Span> = await SpanService.findBy({
261
+ query: {
262
+ traceId: traceId,
263
+ } as never,
264
+ select: {
265
+ spanId: true,
266
+ parentSpanId: true,
267
+ name: true,
268
+ startTimeUnixNano: true,
269
+ durationUnixNano: true,
270
+ statusCode: true,
271
+ kind: true,
272
+ } as never,
273
+ sort: {
274
+ startTimeUnixNano: SortOrder.Ascending,
275
+ } as never,
276
+ limit: MAX_TRACE_SPANS,
277
+ skip: 0,
278
+ props: ctx.props,
279
+ });
280
+
281
+ const isSpanLimitHit: boolean = spans.length >= MAX_TRACE_SPANS;
282
+
283
+ // Build the tree.
284
+ const nodesBySpanId: Map<string, SpanTreeNode> = new Map();
285
+ const roots: Array<SpanTreeNode> = [];
286
+
287
+ for (const span of spans) {
288
+ nodesBySpanId.set(span.spanId?.toString() || "", {
289
+ span,
290
+ children: [],
291
+ });
292
+ }
293
+
294
+ for (const node of nodesBySpanId.values()) {
295
+ const parentId: string = node.span.parentSpanId?.toString() || "";
296
+ const parent: SpanTreeNode | undefined = nodesBySpanId.get(parentId);
297
+ if (parent && parent !== node) {
298
+ parent.children.push(node);
299
+ } else {
300
+ roots.push(node);
301
+ }
302
+ }
303
+
304
+ const lines: Array<string> = [];
305
+
306
+ const renderNode: (node: SpanTreeNode, depth: number) => void = (
307
+ node: SpanTreeNode,
308
+ depth: number,
309
+ ): void => {
310
+ const durationMs: number =
311
+ Math.round((Number(node.span.durationUnixNano) / 1_000_000) * 100) /
312
+ 100;
313
+ const status: string =
314
+ Number(node.span.statusCode) === 2 ? " [ERROR]" : "";
315
+ lines.push(
316
+ `${" ".repeat(depth)}- ${node.span.name} (${durationMs}ms)${status}`,
317
+ );
318
+ for (const child of node.children) {
319
+ renderNode(child, depth + 1);
320
+ }
321
+ };
322
+
323
+ for (const root of roots) {
324
+ renderNode(root, 0);
325
+ }
326
+
327
+ if (isSpanLimitHit) {
328
+ lines.push(
329
+ `… trace truncated at ${MAX_TRACE_SPANS} spans; deeper spans are omitted and some shown here may appear as roots because their parent was cut off.`,
330
+ );
331
+ }
332
+
333
+ const serialized: SerializedResult = ToolResultSerializer.serializeText(
334
+ lines.join("\n"),
335
+ spans.length,
336
+ );
337
+
338
+ /*
339
+ * Build the waterfall widget: each span's start is expressed as an offset in
340
+ * ms from the earliest span start (the trace start), so the renderer can lay
341
+ * out proportional bars without any wall-clock math.
342
+ */
343
+ let traceStartNano: number | undefined = undefined;
344
+ let traceEndNano: number = 0;
345
+ for (const span of spans) {
346
+ const start: number = Number(span.startTimeUnixNano);
347
+ const end: number = start + Number(span.durationUnixNano);
348
+ if (traceStartNano === undefined || start < traceStartNano) {
349
+ traceStartNano = start;
350
+ }
351
+ if (end > traceEndNano) {
352
+ traceEndNano = end;
353
+ }
354
+ }
355
+
356
+ const nanoToMs: (nano: number) => number = (nano: number): number => {
357
+ return Math.round((nano / 1_000_000) * 100) / 100;
358
+ };
359
+
360
+ const widgetSpans: Array<AIChatWidgetSpan> = spans.map((span: Span) => {
361
+ const start: number = Number(span.startTimeUnixNano);
362
+ return {
363
+ spanId: span.spanId?.toString() || "",
364
+ parentSpanId: span.parentSpanId?.toString() || undefined,
365
+ name: span.name || "(unnamed span)",
366
+ startOffsetMs: nanoToMs(start - (traceStartNano ?? start)),
367
+ durationMs: nanoToMs(Number(span.durationUnixNano)),
368
+ isError: Number(span.statusCode) === 2,
369
+ };
370
+ });
371
+
372
+ const totalDurationMs: number =
373
+ traceStartNano !== undefined
374
+ ? nanoToMs(traceEndNano - traceStartNano)
375
+ : 0;
376
+
377
+ return {
378
+ dataForLlm: serialized.text,
379
+ rowCount: serialized.rowCount,
380
+ citationLabel: `Trace ${traceId} (${spans.length}${isSpanLimitHit ? "+" : ""} spans)`,
381
+ citationTarget: {
382
+ type: AIChatCitationTargetType.TraceView,
383
+ params: { traceId: traceId },
384
+ },
385
+ redactionCount: serialized.redactionCount,
386
+ isTruncated: serialized.isTruncated || isSpanLimitHit,
387
+ widget:
388
+ widgetSpans.length > 0
389
+ ? WidgetBuilder.traceWaterfall({
390
+ title: `Trace waterfall (${spans.length}${isSpanLimitHit ? "+" : ""} spans)`,
391
+ description: `${totalDurationMs} ms total`,
392
+ spans: widgetSpans,
393
+ totalDurationMs: totalDurationMs,
394
+ link: {
395
+ type: AIChatCitationTargetType.TraceView,
396
+ params: { traceId: traceId },
397
+ },
398
+ })
399
+ : undefined,
400
+ };
401
+ },
402
+ };
@@ -0,0 +1,189 @@
1
+ import { JSONObject } from "../../../../Types/JSON";
2
+ import {
3
+ AIChatCitationTarget,
4
+ AIChatWidget,
5
+ AIChatWidgetColumn,
6
+ AIChatWidgetSeries,
7
+ AIChatWidgetSpan,
8
+ AIChatWidgetStat,
9
+ AIChatWidgetType,
10
+ } from "../../../../Types/AI/AIChatTypes";
11
+
12
+ /*
13
+ * Builds the inline widgets a tool attaches to its result. Widgets are built
14
+ * from the RAW rows a tool fetched (not the redacted LLM payload) and rendered
15
+ * back to the same user who already has RBAC access to the data.
16
+ *
17
+ * `id` and `citationId` are left blank here — ChatAgentRunner mints them
18
+ * (W1, W2, … and the matching C1, C2, …) after the tool executes so they line
19
+ * up with the server-minted citations.
20
+ */
21
+ export default class WidgetBuilder {
22
+ public static table(params: {
23
+ title: string;
24
+ description?: string | undefined;
25
+ columns: Array<AIChatWidgetColumn>;
26
+ rows: Array<JSONObject>;
27
+ link?: AIChatCitationTarget | undefined;
28
+ }): AIChatWidget {
29
+ return {
30
+ id: "",
31
+ type: AIChatWidgetType.Table,
32
+ title: params.title,
33
+ description: params.description,
34
+ data: {
35
+ columns: params.columns,
36
+ rows: params.rows,
37
+ link: params.link,
38
+ },
39
+ };
40
+ }
41
+
42
+ public static timeSeries(params: {
43
+ title: string;
44
+ description?: string | undefined;
45
+ series: Array<AIChatWidgetSeries>;
46
+ unit?: string | undefined;
47
+ valueLabel?: string | undefined;
48
+ link?: AIChatCitationTarget | undefined;
49
+ }): AIChatWidget {
50
+ return {
51
+ id: "",
52
+ type: AIChatWidgetType.TimeSeriesChart,
53
+ title: params.title,
54
+ description: params.description,
55
+ data: {
56
+ series: params.series,
57
+ xIsTime: true,
58
+ unit: params.unit,
59
+ valueLabel: params.valueLabel,
60
+ link: params.link,
61
+ },
62
+ };
63
+ }
64
+
65
+ public static bars(params: {
66
+ title: string;
67
+ description?: string | undefined;
68
+ series: Array<AIChatWidgetSeries>;
69
+ stacked?: boolean | undefined;
70
+ xIsTime?: boolean | undefined;
71
+ unit?: string | undefined;
72
+ link?: AIChatCitationTarget | undefined;
73
+ }): AIChatWidget {
74
+ return {
75
+ id: "",
76
+ type: AIChatWidgetType.BarChart,
77
+ title: params.title,
78
+ description: params.description,
79
+ data: {
80
+ series: params.series,
81
+ stacked: params.stacked ?? false,
82
+ xIsTime: params.xIsTime ?? false,
83
+ unit: params.unit,
84
+ link: params.link,
85
+ },
86
+ };
87
+ }
88
+
89
+ public static traceWaterfall(params: {
90
+ title: string;
91
+ description?: string | undefined;
92
+ spans: Array<AIChatWidgetSpan>;
93
+ totalDurationMs: number;
94
+ link?: AIChatCitationTarget | undefined;
95
+ }): AIChatWidget {
96
+ return {
97
+ id: "",
98
+ type: AIChatWidgetType.TraceWaterfall,
99
+ title: params.title,
100
+ description: params.description,
101
+ data: {
102
+ spans: params.spans,
103
+ totalDurationMs: params.totalDurationMs,
104
+ link: params.link,
105
+ },
106
+ };
107
+ }
108
+
109
+ public static incidentList(params: {
110
+ title: string;
111
+ description?: string | undefined;
112
+ items: Array<JSONObject>;
113
+ link?: AIChatCitationTarget | undefined;
114
+ }): AIChatWidget {
115
+ return {
116
+ id: "",
117
+ type: AIChatWidgetType.IncidentList,
118
+ title: params.title,
119
+ description: params.description,
120
+ data: { items: params.items, link: params.link },
121
+ };
122
+ }
123
+
124
+ public static alertList(params: {
125
+ title: string;
126
+ description?: string | undefined;
127
+ items: Array<JSONObject>;
128
+ link?: AIChatCitationTarget | undefined;
129
+ }): AIChatWidget {
130
+ return {
131
+ id: "",
132
+ type: AIChatWidgetType.AlertList,
133
+ title: params.title,
134
+ description: params.description,
135
+ data: { items: params.items, link: params.link },
136
+ };
137
+ }
138
+
139
+ public static exceptionList(params: {
140
+ title: string;
141
+ description?: string | undefined;
142
+ items: Array<JSONObject>;
143
+ link?: AIChatCitationTarget | undefined;
144
+ }): AIChatWidget {
145
+ return {
146
+ id: "",
147
+ type: AIChatWidgetType.ExceptionList,
148
+ title: params.title,
149
+ description: params.description,
150
+ data: { items: params.items, link: params.link },
151
+ };
152
+ }
153
+
154
+ public static resourceCard(params: {
155
+ title: string;
156
+ resourceType: string;
157
+ heading: string;
158
+ subheading?: string | undefined;
159
+ fields: Array<{ label: string; value: string }>;
160
+ link?: AIChatCitationTarget | undefined;
161
+ }): AIChatWidget {
162
+ return {
163
+ id: "",
164
+ type: AIChatWidgetType.ResourceCard,
165
+ title: params.title,
166
+ data: {
167
+ resourceType: params.resourceType,
168
+ heading: params.heading,
169
+ subheading: params.subheading,
170
+ fields: params.fields,
171
+ link: params.link,
172
+ },
173
+ };
174
+ }
175
+
176
+ public static stats(params: {
177
+ title: string;
178
+ description?: string | undefined;
179
+ stats: Array<AIChatWidgetStat>;
180
+ }): AIChatWidget {
181
+ return {
182
+ id: "",
183
+ type: AIChatWidgetType.StatCards,
184
+ title: params.title,
185
+ description: params.description,
186
+ data: { stats: params.stats },
187
+ };
188
+ }
189
+ }