@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.
- package/Models/DatabaseModels/AIConversation.ts +322 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
- package/Models/DatabaseModels/AIRun.ts +584 -0
- package/Models/DatabaseModels/AIRunEvent.ts +443 -0
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/LlmLog.ts +26 -0
- package/Models/DatabaseModels/LlmProvider.ts +2 -2
- package/Server/API/AIChatAPI.ts +693 -0
- package/Server/API/LlmProviderAPI.ts +169 -0
- package/Server/API/MicrosoftTeamsAPI.ts +5 -0
- package/Server/API/SlackAPI.ts +663 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/AIConversationMessageService.ts +39 -0
- package/Server/Services/AIConversationService.ts +63 -0
- package/Server/Services/AIRunEventService.ts +39 -0
- package/Server/Services/AIRunService.ts +39 -0
- package/Server/Services/AIService.ts +53 -31
- package/Server/Services/DatabaseService.ts +19 -0
- package/Server/Services/LlmProviderService.ts +110 -0
- package/Server/Services/ProjectService.ts +24 -0
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
- package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1054 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
- package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
- package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
- package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
- package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +204 -0
- package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
- package/Server/Utils/AI/Toolbox/Index.ts +228 -0
- package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
- package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +257 -0
- package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
- package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
- package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
- package/Server/Utils/LLM/LLMService.ts +502 -92
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
- package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
- package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
- package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
- package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +308 -0
- package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
- package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +155 -0
- package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
- package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
- package/Types/AI/AIChatMessageRole.ts +6 -0
- package/Types/AI/AIChatMessageStatus.ts +32 -0
- package/Types/AI/AIChatPermissionMode.ts +69 -0
- package/Types/AI/AIChatTypes.ts +231 -0
- package/Types/AI/AIRunEventType.ts +17 -0
- package/Types/AI/AIRunStatus.ts +28 -0
- package/Types/AI/AIRunType.ts +6 -0
- package/Types/LLM/LlmType.ts +6 -0
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
- package/UI/Components/Page/Page.tsx +14 -0
- package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
- package/UI/Utils/TestLLMProvider.ts +59 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +8 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
- package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
- package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +498 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -0
- package/build/dist/Server/API/LlmProviderAPI.js +107 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +442 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.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/AIConversationMessageService.js +34 -0
- package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
- package/build/dist/Server/Services/AIConversationService.js +42 -0
- package/build/dist/Server/Services/AIConversationService.js.map +1 -0
- package/build/dist/Server/Services/AIRunEventService.js +34 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
- package/build/dist/Server/Services/AIRunService.js +34 -0
- package/build/dist/Server/Services/AIRunService.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +31 -23
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +108 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +745 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +150 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +188 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +372 -88
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
- package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +74 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
- package/build/dist/Types/AI/AIRunEventType.js +18 -0
- package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +26 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
- package/build/dist/Types/AI/AIRunType.js +7 -0
- package/build/dist/Types/AI/AIRunType.js.map +1 -0
- package/build/dist/Types/LLM/LlmType.js +6 -0
- package/build/dist/Types/LLM/LlmType.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +3 -1
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
- package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
- package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { JSONObject, JSONValue } from "../../../../Types/JSON";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Prepares tool results for the LLM prompt: redacts likely secrets/PII,
|
|
5
|
+
* truncates long fields, and caps total payload size. Everything that enters
|
|
6
|
+
* the prompt (and therefore leaves for the LLM provider, and is previewed in
|
|
7
|
+
* LlmLog.requestPrompt) passes through here first.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_ROWS: number = 50;
|
|
11
|
+
const MAX_FIELD_LENGTH: number = 500;
|
|
12
|
+
const MAX_PAYLOAD_BYTES: number = 16 * 1024;
|
|
13
|
+
|
|
14
|
+
interface RedactionRule {
|
|
15
|
+
name: string;
|
|
16
|
+
regex: RegExp;
|
|
17
|
+
replacement: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* These are security redactions, not the fingerprint-normalization regexes
|
|
22
|
+
* (those normalize numbers/uuids for grouping and do not cover credentials).
|
|
23
|
+
*/
|
|
24
|
+
const REDACTION_RULES: Array<RedactionRule> = [
|
|
25
|
+
{
|
|
26
|
+
name: "jwt",
|
|
27
|
+
regex: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/g,
|
|
28
|
+
replacement: "[redacted-jwt]",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "bearer",
|
|
32
|
+
regex: /Bearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi,
|
|
33
|
+
replacement: "Bearer [redacted-token]",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "email",
|
|
37
|
+
regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
|
|
38
|
+
replacement: "[redacted-email]",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "ipv4",
|
|
42
|
+
regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
43
|
+
replacement: "[redacted-ip]",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
/*
|
|
47
|
+
* Long hex blobs are usually secrets (SHA-256 digests, 40+ char API
|
|
48
|
+
* keys). But W3C/OTel trace IDs are exactly 32 hex chars and span IDs
|
|
49
|
+
* exactly 16 — redacting those would break the documented
|
|
50
|
+
* search_logs → get_trace pivot, since the model would never see the ID
|
|
51
|
+
* it needs to pass back. So only redact runs of 33+ hex chars, which
|
|
52
|
+
* preserves both ID lengths while still catching typical hash-length
|
|
53
|
+
* secrets. Labeled secrets of any length are still caught by
|
|
54
|
+
* key-value-secret below.
|
|
55
|
+
*/
|
|
56
|
+
name: "hex-secret",
|
|
57
|
+
regex: /\b[0-9a-fA-F]{33,}\b/g,
|
|
58
|
+
replacement: "[redacted-hex]",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "key-value-secret",
|
|
62
|
+
regex:
|
|
63
|
+
/\b(password|passwd|secret|api[_-]?key|access[_-]?token|authorization)\b(\s*[:=]\s*)("[^"]{4,}"|'[^']{4,}'|[^\s,;&]{4,})/gi,
|
|
64
|
+
replacement: "$1$2[redacted]",
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export interface SerializedResult {
|
|
69
|
+
text: string;
|
|
70
|
+
rowCount: number;
|
|
71
|
+
redactionCount: number;
|
|
72
|
+
isTruncated: boolean;
|
|
73
|
+
bytes: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default class ToolResultSerializer {
|
|
77
|
+
// Single pass per rule: count and substitute ($1/$2 groups) together.
|
|
78
|
+
private static applyRedactionRule(
|
|
79
|
+
text: string,
|
|
80
|
+
rule: RedactionRule,
|
|
81
|
+
): { text: string; count: number } {
|
|
82
|
+
let count: number = 0;
|
|
83
|
+
|
|
84
|
+
const result: string = text.replace(
|
|
85
|
+
rule.regex,
|
|
86
|
+
(...matchArgs: Array<unknown>): string => {
|
|
87
|
+
count++;
|
|
88
|
+
return rule.replacement.replace(
|
|
89
|
+
/\$(\d)/g,
|
|
90
|
+
(_full: string, groupIndex: string): string => {
|
|
91
|
+
return String(matchArgs[Number(groupIndex)] ?? "");
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return { text: result, count };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public static redact(text: string): { text: string; count: number } {
|
|
101
|
+
let redacted: string = text;
|
|
102
|
+
let count: number = 0;
|
|
103
|
+
|
|
104
|
+
for (const rule of REDACTION_RULES) {
|
|
105
|
+
const applied: { text: string; count: number } = this.applyRedactionRule(
|
|
106
|
+
redacted,
|
|
107
|
+
rule,
|
|
108
|
+
);
|
|
109
|
+
redacted = applied.text;
|
|
110
|
+
count += applied.count;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { text: redacted, count };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Byte-accurate truncation (substring counts UTF-16 units, not bytes).
|
|
117
|
+
private static truncateToBytes(text: string, maxBytes: number): string {
|
|
118
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) {
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
return Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private static truncateField(value: string): {
|
|
125
|
+
value: string;
|
|
126
|
+
truncated: boolean;
|
|
127
|
+
} {
|
|
128
|
+
if (value.length <= MAX_FIELD_LENGTH) {
|
|
129
|
+
return { value, truncated: false };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
value: `${value.substring(0, MAX_FIELD_LENGTH)}… [truncated]`,
|
|
133
|
+
truncated: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static serializeValue(value: JSONValue): string {
|
|
138
|
+
if (value === null || value === undefined) {
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
if (value instanceof Date) {
|
|
142
|
+
return value.toISOString();
|
|
143
|
+
}
|
|
144
|
+
if (typeof value === "object") {
|
|
145
|
+
return JSON.stringify(value);
|
|
146
|
+
}
|
|
147
|
+
return String(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* Serialize rows as a compact markdown-ish record list. Applies row cap,
|
|
152
|
+
* per-field truncation, redaction and total payload cap.
|
|
153
|
+
*/
|
|
154
|
+
public static serializeRows(
|
|
155
|
+
rows: Array<JSONObject>,
|
|
156
|
+
totalRowCount?: number,
|
|
157
|
+
): SerializedResult {
|
|
158
|
+
let isTruncated: boolean = false;
|
|
159
|
+
let redactionCount: number = 0;
|
|
160
|
+
|
|
161
|
+
let rowsToSerialize: Array<JSONObject> = rows;
|
|
162
|
+
if (rows.length > MAX_ROWS) {
|
|
163
|
+
rowsToSerialize = rows.slice(0, MAX_ROWS);
|
|
164
|
+
isTruncated = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lines: Array<string> = [];
|
|
168
|
+
|
|
169
|
+
for (const row of rowsToSerialize) {
|
|
170
|
+
const parts: Array<string> = [];
|
|
171
|
+
|
|
172
|
+
for (const key of Object.keys(row)) {
|
|
173
|
+
const rawValue: string = this.serializeValue(row[key] as JSONValue);
|
|
174
|
+
if (!rawValue) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const redacted: { text: string; count: number } = this.redact(rawValue);
|
|
179
|
+
redactionCount += redacted.count;
|
|
180
|
+
|
|
181
|
+
const truncated: { value: string; truncated: boolean } =
|
|
182
|
+
this.truncateField(redacted.text);
|
|
183
|
+
if (truncated.truncated) {
|
|
184
|
+
isTruncated = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
parts.push(`${key}=${truncated.value}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lines.push(`- ${parts.join(" | ")}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let text: string = lines.join("\n");
|
|
194
|
+
|
|
195
|
+
if (Buffer.byteLength(text, "utf8") > MAX_PAYLOAD_BYTES) {
|
|
196
|
+
isTruncated = true;
|
|
197
|
+
while (
|
|
198
|
+
lines.length > 1 &&
|
|
199
|
+
Buffer.byteLength(lines.join("\n"), "utf8") > MAX_PAYLOAD_BYTES
|
|
200
|
+
) {
|
|
201
|
+
lines.pop();
|
|
202
|
+
}
|
|
203
|
+
// A single oversized row can still exceed the cap — hard-slice it.
|
|
204
|
+
text = `${this.truncateToBytes(lines.join("\n"), MAX_PAYLOAD_BYTES)}\n… [payload truncated]`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rowCount: number = totalRowCount ?? rows.length;
|
|
208
|
+
|
|
209
|
+
if (rows.length === 0) {
|
|
210
|
+
text = "(no rows found)";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/*
|
|
214
|
+
* Tell the model when it is only seeing a slice. Without this marker a
|
|
215
|
+
* capped result looks complete, and the model reports partial data as if it
|
|
216
|
+
* were the whole picture.
|
|
217
|
+
*/
|
|
218
|
+
if (rows.length > MAX_ROWS) {
|
|
219
|
+
text = `${text}\n… [showing the first ${MAX_ROWS} of ${rowCount} rows; narrow the query — a shorter time range or more filters — to see the rest]`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
text,
|
|
224
|
+
rowCount,
|
|
225
|
+
redactionCount,
|
|
226
|
+
isTruncated,
|
|
227
|
+
bytes: Buffer.byteLength(text, "utf8"),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Serialize free-form text (e.g. a rendered trace tree).
|
|
232
|
+
public static serializeText(
|
|
233
|
+
text: string,
|
|
234
|
+
rowCount: number,
|
|
235
|
+
): SerializedResult {
|
|
236
|
+
const redacted: { text: string; count: number } = this.redact(text);
|
|
237
|
+
let output: string = redacted.text;
|
|
238
|
+
let isTruncated: boolean = false;
|
|
239
|
+
|
|
240
|
+
if (Buffer.byteLength(output, "utf8") > MAX_PAYLOAD_BYTES) {
|
|
241
|
+
output = `${this.truncateToBytes(output, MAX_PAYLOAD_BYTES)}… [payload truncated]`;
|
|
242
|
+
isTruncated = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!output) {
|
|
246
|
+
output = "(no data found)";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
text: output,
|
|
251
|
+
rowCount,
|
|
252
|
+
redactionCount: redacted.count,
|
|
253
|
+
isTruncated,
|
|
254
|
+
bytes: Buffer.byteLength(output, "utf8"),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
2
|
+
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
3
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
4
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
5
|
+
import OneUptimeDate from "../../../../Types/Date";
|
|
6
|
+
import Permission from "../../../../Types/Permission";
|
|
7
|
+
import {
|
|
8
|
+
AIChatCitationTarget,
|
|
9
|
+
AIChatWidget,
|
|
10
|
+
} from "../../../../Types/AI/AIChatTypes";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* The execution context for a tool call. projectId is ALWAYS the tenant from
|
|
14
|
+
* the authenticated request props — it is never a tool argument, so the model
|
|
15
|
+
* cannot point a tool at another project.
|
|
16
|
+
*/
|
|
17
|
+
export interface ToolContext {
|
|
18
|
+
projectId: ObjectID;
|
|
19
|
+
props: DatabaseCommonInteractionProps;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ToolExecutionResult {
|
|
23
|
+
// Serialized, truncated and redacted data that goes into the LLM prompt.
|
|
24
|
+
dataForLlm: string;
|
|
25
|
+
rowCount: number;
|
|
26
|
+
citationLabel: string;
|
|
27
|
+
citationTarget?: AIChatCitationTarget | undefined;
|
|
28
|
+
redactionCount: number;
|
|
29
|
+
isTruncated: boolean;
|
|
30
|
+
/*
|
|
31
|
+
* Optional structured widget (chart, table, trace waterfall, resource card)
|
|
32
|
+
* rendered inline in the chat. Built from the RAW rows the tool fetched — not
|
|
33
|
+
* the redacted LLM payload — because it is rendered back to the same user who
|
|
34
|
+
* already has RBAC access to this data.
|
|
35
|
+
*/
|
|
36
|
+
widget?: AIChatWidget | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ObservabilityTool {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
// JSON Schema for the tool arguments, handed to the LLM.
|
|
43
|
+
inputSchema: JSONObject;
|
|
44
|
+
/*
|
|
45
|
+
* The user must hold at least one of these permissions for the tool to
|
|
46
|
+
* execute. For tools that call permission-checked services this is
|
|
47
|
+
* defense-in-depth; for tools that call raw-SQL aggregation services it is
|
|
48
|
+
* the authorization.
|
|
49
|
+
*/
|
|
50
|
+
requiredPermissions: Array<Permission>;
|
|
51
|
+
/*
|
|
52
|
+
* True for tools that MUTATE project data (create/acknowledge/resolve, etc).
|
|
53
|
+
* These are gated by the conversation's permission mode: paused for approval
|
|
54
|
+
* in AskForApproval mode, run immediately in AutoRun, and not offered to the
|
|
55
|
+
* model at all in ReadOnly. Undefined/false = a read-only tool that always
|
|
56
|
+
* runs.
|
|
57
|
+
*/
|
|
58
|
+
isMutation?: boolean | undefined;
|
|
59
|
+
/*
|
|
60
|
+
* Optional human-readable label for the approval card, derived from the
|
|
61
|
+
* (already validated) tool arguments — e.g. "Create incident: Checkout down".
|
|
62
|
+
* Falls back to the tool name when omitted.
|
|
63
|
+
*/
|
|
64
|
+
buildActionTitle?: ((args: JSONObject) => string) | undefined;
|
|
65
|
+
execute(args: JSONObject, ctx: ToolContext): Promise<ToolExecutionResult>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* Defensive argument readers. Tool arguments come from the LLM and are
|
|
70
|
+
* untrusted: everything is type-checked, defaulted and clamped.
|
|
71
|
+
*/
|
|
72
|
+
export class ToolArgs {
|
|
73
|
+
public static getString(args: JSONObject, key: string): string | undefined {
|
|
74
|
+
const value: unknown = args[key];
|
|
75
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
76
|
+
return value.trim();
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public static getStringArray(
|
|
82
|
+
args: JSONObject,
|
|
83
|
+
key: string,
|
|
84
|
+
): Array<string> | undefined {
|
|
85
|
+
const value: unknown = args[key];
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
const strings: Array<string> = value.filter((item: unknown) => {
|
|
88
|
+
return typeof item === "string" && item.trim().length > 0;
|
|
89
|
+
}) as Array<string>;
|
|
90
|
+
if (strings.length > 0) {
|
|
91
|
+
return strings;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public static getNumber(
|
|
98
|
+
args: JSONObject,
|
|
99
|
+
key: string,
|
|
100
|
+
options: { defaultValue: number; min: number; max: number },
|
|
101
|
+
): number {
|
|
102
|
+
const value: unknown = args[key];
|
|
103
|
+
let parsed: number = options.defaultValue;
|
|
104
|
+
|
|
105
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
106
|
+
parsed = value;
|
|
107
|
+
} else if (typeof value === "string" && value.trim().length > 0) {
|
|
108
|
+
const fromString: number = Number(value);
|
|
109
|
+
if (Number.isFinite(fromString)) {
|
|
110
|
+
parsed = fromString;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Math.min(options.max, Math.max(options.min, Math.floor(parsed)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public static getBoolean(args: JSONObject, key: string): boolean | undefined {
|
|
118
|
+
const value: unknown = args[key];
|
|
119
|
+
if (typeof value === "boolean") {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
if (value === "true") {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (value === "false") {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public static getObjectID(
|
|
132
|
+
args: JSONObject,
|
|
133
|
+
key: string,
|
|
134
|
+
): ObjectID | undefined {
|
|
135
|
+
const value: string | undefined = this.getString(args, key);
|
|
136
|
+
if (!value) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return new ObjectID(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/*
|
|
143
|
+
* Combines the caller's optional serviceId filter with the user's owned-scope
|
|
144
|
+
* access (from ModelPermission.getAccessibleServiceIdsForAnalyticsModel) into
|
|
145
|
+
* the `serviceIds` filter to hand an aggregation service.
|
|
146
|
+
*
|
|
147
|
+
* `allowed === null` means the user has project-wide access: pass only the
|
|
148
|
+
* caller's own filter (or undefined for no filter). Otherwise the user is
|
|
149
|
+
* label/owned-restricted: intersect with any requested service and NEVER
|
|
150
|
+
* return undefined or an empty array — an empty result is forced to a
|
|
151
|
+
* no-match sentinel, because the aggregation services treat a missing/empty
|
|
152
|
+
* serviceIds as "no filter" (which would leak the whole project).
|
|
153
|
+
*/
|
|
154
|
+
public static scopeServiceIds(
|
|
155
|
+
allowed: Array<ObjectID> | null,
|
|
156
|
+
requested: ObjectID | undefined,
|
|
157
|
+
): Array<ObjectID> | undefined {
|
|
158
|
+
if (allowed === null) {
|
|
159
|
+
return requested ? [requested] : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let effective: Array<ObjectID> = allowed;
|
|
163
|
+
if (requested) {
|
|
164
|
+
effective = allowed.filter((id: ObjectID) => {
|
|
165
|
+
return id.toString() === requested.toString();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (effective.length === 0) {
|
|
170
|
+
return [ObjectID.getZeroObjectID()];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return effective;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* Time range for a tool call: explicit ISO startTime/endTime arguments,
|
|
178
|
+
* clamped to a maximum window, defaulting to the last hour.
|
|
179
|
+
*/
|
|
180
|
+
public static getTimeRange(
|
|
181
|
+
args: JSONObject,
|
|
182
|
+
options?: { defaultHours?: number; maxDays?: number },
|
|
183
|
+
): { startTime: Date; endTime: Date } {
|
|
184
|
+
const defaultHours: number = options?.defaultHours ?? 1;
|
|
185
|
+
const maxDays: number = options?.maxDays ?? 30;
|
|
186
|
+
|
|
187
|
+
const endTimeString: string | undefined = this.getString(args, "endTime");
|
|
188
|
+
const startTimeString: string | undefined = this.getString(
|
|
189
|
+
args,
|
|
190
|
+
"startTime",
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* Reject unparseable timestamps instead of silently falling back to the
|
|
195
|
+
* default window — a silent fallback answers a different question than the
|
|
196
|
+
* one asked. Throwing lets the model see the error and retry with a valid
|
|
197
|
+
* ISO 8601 value.
|
|
198
|
+
*/
|
|
199
|
+
let endTime: Date = OneUptimeDate.getCurrentDate();
|
|
200
|
+
if (endTimeString) {
|
|
201
|
+
const parsed: Date = new Date(endTimeString);
|
|
202
|
+
if (isNaN(parsed.getTime())) {
|
|
203
|
+
throw new BadDataException(
|
|
204
|
+
`endTime "${endTimeString}" is not a valid ISO 8601 timestamp (e.g. 2024-01-31T14:00:00Z).`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
endTime = parsed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let startTime: Date = OneUptimeDate.addRemoveHours(
|
|
211
|
+
endTime,
|
|
212
|
+
-1 * defaultHours,
|
|
213
|
+
);
|
|
214
|
+
if (startTimeString) {
|
|
215
|
+
const parsed: Date = new Date(startTimeString);
|
|
216
|
+
if (isNaN(parsed.getTime())) {
|
|
217
|
+
throw new BadDataException(
|
|
218
|
+
`startTime "${startTimeString}" is not a valid ISO 8601 timestamp (e.g. 2024-01-31T13:00:00Z).`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
startTime = parsed;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (startTime.getTime() >= endTime.getTime()) {
|
|
225
|
+
throw new BadDataException("startTime must be before endTime.");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const maxWindowMs: number = maxDays * 24 * 60 * 60 * 1000;
|
|
229
|
+
if (endTime.getTime() - startTime.getTime() > maxWindowMs) {
|
|
230
|
+
startTime = new Date(endTime.getTime() - maxWindowMs);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { startTime, endTime };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Shared JSON-Schema fragments for tool input schemas.
|
|
238
|
+
export const TimeRangeSchemaProperties: JSONObject = {
|
|
239
|
+
startTime: {
|
|
240
|
+
type: "string",
|
|
241
|
+
description:
|
|
242
|
+
"Start of the time range as an ISO 8601 timestamp. Defaults to one hour before endTime.",
|
|
243
|
+
},
|
|
244
|
+
endTime: {
|
|
245
|
+
type: "string",
|
|
246
|
+
description:
|
|
247
|
+
"End of the time range as an ISO 8601 timestamp. Defaults to now.",
|
|
248
|
+
},
|
|
249
|
+
};
|