// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import type { ContentBlock } from "@/ts/interfaces/chat";
import type { TranslateFn } from "@/types/i18n";
// `response` is stamped onto blocks by the stream handler but is not part
// of the shared ContentBlock interface.
export type ToolCallBlock = ContentBlock & { response?: Record };
export function truncateQuery(query: string) {
if (!query) return "";
const maxLength = 100;
if (query.length <= maxLength) return query;
return query.substring(0, maxLength) + "...";
}
export function formatContextKey(key: string) {
return key
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
export function formatContextValue(value: any) {
if (typeof value === "string") {
if (value.length > 30) return value.substring(0, 30) + "...";
return value;
}
return String(value);
}
// Extract display fields from tool call context (handles different tool schemas)
export function getToolCallDisplayData(context: any) {
if (!context) return null;
const data: Record = {};
// Handle nested request_body.query structure (SearchSQL, ExtractPatterns)
if (context.request_body?.query) {
const q = context.request_body.query;
if (q.sql) data.query = q.sql;
if (q.start_time) data.start_time = q.start_time;
if (q.end_time) data.end_time = q.end_time;
if (q.from !== undefined) data.from = q.from;
if (q.size !== undefined) data.size = q.size;
if (q.query_type) data.query_type = q.query_type;
if (q.vrl) data.vrl = q.vrl;
}
// Handle testFunction context (VRL validation)
if (context.vrl) data.vrl = context.vrl;
if (context.request_body?.function) data.vrl = context.request_body.function;
// Handle flat SQL from SearchSQL enriched context
if (context.sql) data.query = context.sql;
// Handle flat structure (StreamSchema, etc.)
if (context.stream_name) data.stream = context.stream_name;
if (context.type) data.type = context.type;
// CLI tools: surface the command string
if (context.command) data.command = context.command;
return Object.keys(data).length > 0 ? data : null;
}
export function hasToolCallDetails(block: ToolCallBlock) {
if (block.success === false) return true;
if (block.summary) return true;
if (block.response) return true;
return getToolCallDisplayData(block.context) !== null;
}
// Splits an already-interpolated sentence around the value that renders bold,
// so the header stays ONE translatable sentence instead of two fragments.
export function splitAroundHighlight(full: string, highlight: string) {
const at = full.indexOf(highlight);
if (at < 0) return { text: full, highlight: null as string | null, suffix: "" };
return {
text: full.slice(0, at),
highlight: highlight as string | null,
suffix: full.slice(at + highlight.length),
};
}
export function formatTimestamp(timestamp: number, t: TranslateFn) {
if (!timestamp || timestamp === 0) return t("aiAssistant.aiChat.notSpecified");
// Timestamp is in microseconds, convert to milliseconds
const ms = timestamp > 1e15 ? timestamp / 1000 : timestamp;
const date = new Date(ms);
return date.toLocaleString();
}
export function formatToolCallMessage(block: ToolCallBlock, t: TranslateFn) {
if (block.tool === "testFunction") {
if (block.success === false) {
return {
text: t("aiAssistant.aiChat.toolVrlValidationFailed"),
highlight: null,
suffix: "",
};
}
return { text: t("aiAssistant.aiChat.toolVrlValidated"), highlight: null, suffix: "" };
}
if (block.tool === "SearchSQL") {
if (block.success === false) {
return {
text: t("aiAssistant.aiChat.toolQueryFailed"),
highlight: null,
suffix: "",
};
}
if (block.response?.total !== undefined) {
const streamType = block.context?.type || "logs";
return {
text: t("aiAssistant.aiChat.toolQueriedStream", { type: streamType }),
highlight: t("aiAssistant.aiChat.toolResultsCount", {
count: block.response.total,
}),
suffix: "",
};
}
}
if (block.tool === "StreamSchema" && block.context?.stream_name) {
const streamName = block.context.stream_name;
return splitAroundHighlight(
t("aiAssistant.aiChat.toolStreamSchema", { name: streamName }),
streamName,
);
}
if (block.tool === "GetIncident" && block.context?.incident_id) {
const incidentId = block.context.incident_id;
return splitAroundHighlight(
t("aiAssistant.aiChat.toolGetIncident", { id: incidentId }),
incidentId,
);
}
if (block.tool === "GetAlert" && block.context?.alert_id) {
const alertId = block.context.alert_id;
return splitAroundHighlight(t("aiAssistant.aiChat.toolGetAlert", { id: alertId }), alertId);
}
if (block.tool === "GetDashboard" && block.context?.dashboard_id) {
const dashboardId = block.context.dashboard_id;
return splitAroundHighlight(
t("aiAssistant.aiChat.toolGetDashboard", { id: dashboardId }),
dashboardId,
);
}
// List tools: show count from normalized { total, items } response
if (block.response?.total !== undefined && block.success !== false) {
const base = block.message || block.tool || t("aiAssistant.aiChat.toolListedFallback");
return {
text: base + " ",
highlight: t("aiAssistant.aiChat.toolFoundCount", { count: block.response.total }),
suffix: "",
};
}
if (block.success === false && block.resultMessage) {
const msg =
block.resultMessage.length > 60
? block.resultMessage.substring(0, 60) + "..."
: block.resultMessage;
return { text: msg, highlight: null, suffix: "" };
}
if (block.success !== false && block.summary?.count !== undefined) {
const base = block.message || block.tool || t("aiAssistant.aiChat.toolFallback");
return {
text: base + " ",
highlight: t("aiAssistant.aiChat.toolResultsCount", { count: block.summary.count }),
suffix: "",
};
}
return { text: block.message, highlight: null, suffix: "" };
}