Files
openobserve/web/scripts/assistantBundlePlugin.ts
T
Harsh Mahajan 4fc65470e5 feat(ai-assistant): assistant revamp
Web:
- Shell, turn engine and conversation view rebuilt around a single assistant store
- Evidence, approvals, chat history, sharing, doors (Explain/Troubleshoot/Add to chat), settings and credits
- Approval card survives a reload; Escape yields to open menus

Backend:
- Typed AI error envelope, resumable turns and capabilities endpoint
- Org admin controls, chat history records, credits and approval policy
- Query-context evidence and explicit chat mode
- AI migrations registered after main's, as schema versions 129-133

Claude-Session: https://claude.ai/code/session_017PUuNehbpAvsekHyhEBmFt
2026-10-30 13:54:19 +05:30

102 lines
4.3 KiB
TypeScript

// 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 <http://www.gnu.org/licenses/>.
import type { Plugin } from "vite";
const ENTRY = /\/src\/main\.ts$/;
const PAGE = /\/src\/views\/SharedChatView\.vue$/;
const FORBIDDEN = [
/\/composables\/assistant\/(runtime|assistantStore|turnEngine|useAssistantClient|index)\.ts$/,
/\/services\/assistant\.queries\.ts$/,
];
// Each edge sits behind a branch the public page never takes; the sharing E2E checks the network for it.
const GATED_EDGES: [RegExp, RegExp][] = [
// initAssistant, which main.ts skips on /ai/public/.
[ENTRY, /\/composables\/assistant\/index\.ts$/],
// ensureRuntime, called only by host editors and the panel width, which the page never mounts.
[/\/composables\/assistant\/runtimeHandle\.ts$/, /\/composables\/assistant\/runtime\.ts$/],
// The gate states, which only the org page shows (forbidden, not configured).
[/\/shared\/SharedChatState\.vue(\?.*)?$/, /\/gate\/AssistantGateState\.vue$/],
// The message bar and inline editor, which a readonly transcript never mounts.
[/\/transcript\/AssistantTranscript\.vue(\?.*)?$/, /\/transcript\/AssistantMessageActions\.vue$/],
// Ask an admin, which a readonly turn outcome replaces with one line.
[/\/turn\/AssistantTurnOutcome\.vue(\?.*)?$/, /\/gate\/AssistantAskAdminPopover\.vue$/],
];
interface ModuleEdges {
importedIds: readonly string[];
dynamicallyImportedIds: readonly string[];
}
/** Fails the build when the public shared page can reach the assistant runtime, printing the import chain. */
export function assistantBundlePlugin(): Plugin {
return {
name: "o2-assistant-bundle-check",
apply: "build",
generateBundle() {
const ids = [...this.getModuleIds()];
const unmatched = FORBIDDEN.filter((re) => !ids.some((id) => re.test(id)));
if (unmatched.length) this.error(`no module matches ${unmatched.join(", ")}`);
const entry = ids.find((id) => ENTRY.test(id));
const page = ids.find((id) => PAGE.test(id));
if (!entry || !page) this.error("main.ts or SharedChatView.vue is missing from the build");
const edges = (id: string): ModuleEdges | null => this.getModuleInfo(id);
const app = walk(entry as string, edges, false);
const shared = walk(page as string, edges, true);
const failures = [...app.failures, ...shared.failures];
if (failures.length) {
this.error(`the public shared page reaches the runtime:\n${failures.join("\n")}`);
}
this.info(`public shared page: ${app.seen + shared.seen} modules, none of the runtime`);
},
};
}
// From main.ts only its own dynamic imports run on every page; the routes behind the router are lazy.
function walk(
start: string,
edges: (id: string) => ModuleEdges | null,
followDynamic: boolean,
): { failures: string[]; seen: number } {
const chain = new Map<string, string[]>([[start, [start]]]);
const queue = [start];
const failures: string[] = [];
while (queue.length) {
const id = queue.shift() as string;
const info = edges(id);
if (!info) continue;
const next = [...info.importedIds];
if (followDynamic || id === start) next.push(...info.dynamicallyImportedIds);
for (const to of next) {
if (chain.has(to) || GATED_EDGES.some(([from, target]) => from.test(id) && target.test(to))) {
continue;
}
const path = [...(chain.get(id) as string[]), to];
chain.set(to, path);
if (FORBIDDEN.some((re) => re.test(to))) failures.push(` ${path.map(short).join(" -> ")}`);
else queue.push(to);
}
}
return { failures, seen: chain.size };
}
function short(id: string): string {
const at = id.indexOf("/src/");
return at >= 0 ? id.slice(at + 1) : id;
}