diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index fe77ac1..17fa9e2 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -13,6 +13,7 @@
# along with this program. If not, see .
import os
+import json
import re
import logging
from typing import Optional
@@ -438,6 +439,20 @@ def _parse_or_repair(self, parser, text, caller_name):
if not m:
raise
candidate = m.group()
+ # Some models (e.g. gemini-3.5-flash) echo back the JSON schema
+ # wrapper: {"properties": {"field": value, ...}} instead of just
+ # {"field": value, ...}. Unwrap one level before retrying.
+ try:
+ parsed_obj = json.loads(candidate)
+ if (
+ isinstance(parsed_obj, dict)
+ and set(parsed_obj.keys()) <= {"properties", "required", "title", "type", "description"}
+ and "properties" in parsed_obj
+ and isinstance(parsed_obj["properties"], dict)
+ ):
+ candidate = json.dumps(parsed_obj["properties"])
+ except Exception:
+ pass
try:
return parser.parse(candidate)
except OutputParserException:
@@ -1013,7 +1028,7 @@ def select_retriever_prompt(self):
_AGENTIC_AGENT_SYSTEM = """\
You are a GraphRAG agent answering questions over a TigerGraph knowledge graph.
-You have a set of read-only tools (graph schema via graphrag__get_schema, structural query generation, several unstructured retrievers, raw GSQL via tg_run_query, neighbor expansion). The graph schema is NOT pre-loaded — fetch it with graphrag__get_schema when you need it.
+You have a set of read-only tools (graph schema via graphrag__get_schema, registered installed GSQL tools named graphrag__gsql__* when present, structural query generation, several unstructured retrievers, raw GSQL via tg_run_query, neighbor expansion). The graph schema is NOT pre-loaded — fetch it with graphrag__get_schema when you need it for structural or unstructured retrieval. Registered GSQL tools do not need the schema first.
REASON, ACT, OBSERVE — repeat until you can give a complete, well-grounded answer.
@@ -1038,7 +1053,11 @@ def select_retriever_prompt(self):
# Operator-customizable retrieval strategy for the react agent: the first
# action, then each next action driven by what the previous result returned.
_AGENTIC_AGENT_USER_DEFAULT = """\
-- For most questions, make your FIRST action a vector search (graphrag__hybrid_search or graphrag__contextual_search) — it gives the broadest grounding. Skip it only when you are highly confident the question is a pure structured-data request (an exact count, an attribute/id lookup, a relationship traversal, or an aggregation over typed graph data) that a generated graph query fully answers on its own.
+- Before calling any retrieval tool, check whether the question is self-contained: can it be fully understood without reading ## Conversation? If the subject, entity, or topic is not named explicitly in the question, look up the most recent relevant entity from ## Conversation and substitute its full name into every retrieval call. If ## Conversation contains multiple candidates and it is genuinely unclear which one the user means, ask one short clarifying question instead of guessing — do not call any retrieval tool until clarified.
+- When calling an unstructured retriever (hybrid, contextual, similarity, community), pass only the sub-question for that specific part as a standalone search query in the user's language. Do not pass the full multi-part question, a part already covered by another step, or an unresolved reference from conversation history.
+- If a graphrag__gsql__* tool is available and its description matches the question, you may call it. If none match, ignore those tools. Do not call a list/register tool first, and do not call a gsql tool first unless its description matches.
+- A description match on one clause is enough to call the GSQL tool. If the question has other parts that still need passages or typed graph facts, call hybrid/community/structural for those parts too — do not stop after the GSQL tool.
+- For most other questions, make your FIRST action a vector search (graphrag__hybrid_search or graphrag__contextual_search) — it gives the broadest grounding. Skip it only when you are highly confident the question is a pure structured-data request (an exact count, an attribute/id lookup, a relationship traversal, an aggregation over typed graph data, or a matching graphrag__gsql__* tool) that a graph query fully answers on its own.
- Let each observation drive the next action: if the passages you got back name specific entities or relationships you still need hard facts about, follow up with a structural query; if a result is thin, empty, or off-target, widen its parameters (top_k, num_hops) or switch method rather than repeating the same call.
- Before answering, check that every part of the question is covered with the specific facts and figures it asks for; if a required value, table, or entity is still missing, retrieve again (widen top_k / num_hops or switch method) rather than answering vaguely or partially.
- For a specific value, row, total, ranking, or year-over-year comparison, use graphrag__hybrid_search or graphrag__contextual_search with top_k >= 10 (they return atomic table chunks that keep full row/column structure), and quote the exact label, column, year, or unit from the question so the retriever can match it."""
@@ -1067,13 +1086,15 @@ def agentic_agent_prompt(self):
The graph schema is NOT provided here — the structural and unstructured query tools load it themselves at run time, so plan retrieval steps directly. A question that needs no graph data should not include any graph-retrieval step (plan only the final answer step, or the relevant non-graph tool).
-You have two kinds of retrieval:
+You have three kinds of retrieval:
+- INSTALLED (graphrag__gsql__*): a user-registered installed GSQL query. Use it only when that tool's description matches the question. Do not call one just because it is listed, and do not call a lookup/list tool first.
- STRUCTURAL (graphrag__structural_retrieve): generates and runs a graph query. Best for counts, lookups by attribute/id, relationships, and aggregations over typed data. It depends on the LLM generating a correct query against the live schema — it can return nothing or the wrong rows when the question doesn't map cleanly to typed graph data, so it is NOT a safe sole source of context.
- UNSTRUCTURED (graphrag__hybrid_search / similarity_search / contextual_search / community_search): vector search over document text. Best for "what/why/how/describe/summarize" questions answered from passages. community_search suits broad/overall questions.
Plan mechanics (fixed):
- A later step may depend on an earlier one: set depends_on and use arg_bindings to pull a value from a prior step's result, e.g. {"question": "S1.context.result"}.
- Retrieval params (top_k, num_hops, community_level) are optional; omit them to use defaults, or set higher values when you expect a broad answer.
+- For each unstructured step, set args.question to a natural-language question covering that clause only, in the user's language — phrase it the way a person would ask it, not as a keyword list. Do not include the full multi-part question, any topic or term that belongs to a clause already assigned to another step (INSTALLED, STRUCTURAL, or a prior unstructured step), or an unresolved reference from conversation history.
- The final step MUST have kind="answer" and tool="" (the orchestrator synthesizes the answer from gathered context); it should depend_on all retrieval steps.
Decide which retrievals to include, how many, and in what order using the "Retrieval Strategy" below. Return ONLY the structured plan.
@@ -1088,8 +1109,14 @@ def agentic_agent_prompt(self):
# Strategy (operator-customizable) — moved out of the fixed rules so it can
# be tuned without touching the role / act model / plan mechanics.
_AGENTIC_PLANNER_USER_DEFAULT = """\
-- Prioritize including at least one vector search step (graphrag__hybrid_search or graphrag__contextual_search) unless you are highly confident the question is a pure structured-data request — an exact count, an attribute/id lookup, a relationship traversal, or an aggregation over typed graph data — that a generated graph query fully answers on its own. Whenever the answer could plausibly live in document text (what/why/how/describe/summarize, definitions, explanations, figures), include a vector search step. When unsure, include vector search.
-- Use BOTH kinds when a question needs facts from the graph AND supporting text; you may run several of each, in any order. When you use STRUCTURAL, pair it with a vector search step unless the question is a pure structured-data request.
+- Before building the plan, check whether the question is self-contained: can it be fully understood without reading ## Conversation? If the subject, entity, or topic is not named explicitly in the question, find the most recent relevant entity from ## Conversation and substitute its full name in every step's args. If ## Conversation has multiple candidates and it is genuinely unclear which one the user means, plan only a final answer step (no retrieval) that asks the user one short clarifying question.
+- When a question has multiple independent clauses, assign each clause to its own retrieval step. Clauses are independent when each can be answered without the other's result. If answering one sub-question requires the answer to another (a reasoning chain), treat the whole question as a single retrieval — do not decompose a reasoning chain into multiple steps.
+- If a graphrag__gsql__* tool is in the catalog and its description matches the question, include that tool. If none match, ignore them and plan hybrid/community/structural exactly as today. Do not call a list/register tool; do not call a gsql tool first unless its description matches.
+- If the entire question is fully answered by a matching graphrag__gsql__* tool, plan ONLY that tool + the answer step — do not add any vector search step.
+- You may pair a graphrag__gsql__* tool with a vector search step only when the question has a separate clause that requires document passages beyond what the GSQL tool returns.
+- A description match on one clause is enough. If another clause still needs passages or typed graph facts, plan hybrid/community/structural for that clause too.
+- Prioritize including at least one vector search step (graphrag__hybrid_search or graphrag__contextual_search) unless the question is fully answered by a matching graphrag__gsql__* tool or is a pure structured-data request (an exact count, an attribute/id lookup, a relationship traversal, or an aggregation over typed graph data). Whenever the answer could plausibly live in document text (what/why/how/describe/summarize, definitions, explanations, figures), include a vector search step. When unsure, include vector search.
+- Use BOTH structural and unstructured kinds when a question needs facts from the graph AND supporting text; you may run several of each, in any order. When you use STRUCTURAL, pair it with a vector search step unless the question is a pure structured-data request.
- Prefer the smallest plan that will work. Trivial/greeting questions need only the final answer step.
- Tabular / numeric questions (a specific value, a row, a column total, a ranking, or a year-over-year comparison from a table or chart): prefer graphrag__contextual_search or graphrag__hybrid_search with top_k>=10 (these return atomic table chunks that preserve full row/column structure); avoid graphrag__similarity_search alone; quote any specific table label, column header, year, or unit from the question (e.g. "ROE 2023"); for "compare X across years/regions/categories" set top_k>=15."""
@@ -1200,6 +1227,7 @@ def hyde_prompt(self):
- **Quote exact values from the source.** Numbers, units, time periods, and named entities must appear verbatim — do not round, approximate, or translate units. Keep units in their original format, script, and language. For example, if the source says `1,234 km`, write `1,234 km`, not `767 miles` or `about 1,200 km`.
- **For comparison or "which is the highest" questions, list each candidate's value before stating the conclusion.** Show the working — do not jump directly to a one-line answer.
- **Score** each context for relevance and use only the high-scoring ones; do not invent additional logic.
+- **Multi-part questions:** answer each part from its matching retrieval context. Do not mix structured-query results with document-passage results when answering different parts. If retrieved context is off-topic for a part, say that part is not covered by the retrieved information.
- **Cover** the relevant information, especially image references that carry critical visual information.
- **Format** the answer in Markdown — titles, paragraphs, bulleted / numbered lists, images, and tables. Place images and tables below the related text section.
- **Tables**: every row, including the header, starts on a new line.
diff --git a/graphrag-ui/src/pages/setup/KGAdmin.tsx b/graphrag-ui/src/pages/setup/KGAdmin.tsx
index 2cf23b5..eb1213d 100644
--- a/graphrag-ui/src/pages/setup/KGAdmin.tsx
+++ b/graphrag-ui/src/pages/setup/KGAdmin.tsx
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TagInput, TypeHint } from "@/components/ui/tag-input";
-import { Database, Loader2, RefreshCw, Upload, Wrench } from "lucide-react";
+import { Database, Loader2, RefreshCw, Upload, Wrench, FileCode, List } from "lucide-react";
import { pauseIdleTimer, resumeIdleTimer, pingIdleTimer } from "@/hooks/useIdleTimeout";
import {
Dialog,
@@ -19,6 +19,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useConfirm } from "@/hooks/useConfirm";
import { useAlert } from "@/hooks/useAlert";
import { resolveUploadConflicts } from "@/utils/uploadConflicts";
@@ -35,6 +36,37 @@ const INPUT_CLIP_FIX: React.CSSProperties = {
lineHeight: "1.5",
};
+type QueryDraft = {
+ name: string;
+ returns: string;
+ useFor: string;
+ doNotUse: string;
+ gsql: string;
+};
+type ListedQuery = { function_header: string; description: string; docstring?: string };
+
+function emptyQueryDraft(): QueryDraft {
+ return { name: "", returns: "", useFor: "", doNotUse: "", gsql: "" };
+}
+
+/** Same 1–2 sentence shape as built-in tools in tool_registry.py. */
+function buildToolDescription(
+ name: string,
+ returns: string,
+ useFor: string,
+ doNotUse: string
+): string {
+ const n = name.trim() || "this_query";
+ const r = returns.trim().replace(/\.+$/, "");
+ const u = useFor.trim().replace(/\.+$/, "");
+ const d = doNotUse.trim().replace(/\.+$/, "");
+ const parts = [`Run installed query ${n}.`];
+ if (r) parts.push(`Returns ${r}.`);
+ if (u) parts.push(`Use for ${u}.`);
+ if (d) parts.push(`Do not use for ${d}.`);
+ return parts.join(" ");
+}
+
/**
* Returns a human-readable error string when a graph name violates naming rules,
* or null when the name is valid.
@@ -68,6 +100,7 @@ const KGAdmin = () => {
const [refreshDialogOpen, setRefreshDialogOpen] = useState(false);
const [ingestDialogOpen, setIngestDialogOpen] = useState(false);
const [migrationDialogOpen, setMigrationDialogOpen] = useState(false);
+ const [registerDialogOpen, setRegisterDialogOpen] = useState(false);
// Migration Assistant state
const [migrationGraph, setMigrationGraph] = useState("");
@@ -79,19 +112,23 @@ const KGAdmin = () => {
missing_files: string[];
};
needs_repair?: boolean;
- embeddings?: {
- by_type: Record;
- total_missing: number;
- };
- embeddings_incomplete?: boolean;
- community_summaries?: { total: number; needs_resummarize: number };
- community_summaries_incomplete?: boolean;
} | null>(null);
const [migrationChecking, setMigrationChecking] = useState(false);
const [migrationApplying, setMigrationApplying] = useState(false);
- // "" | "regenerate_embeddings" | "regenerate_summaries" — which regen is running
- const [migrationRegenerating, setMigrationRegenerating] = useState("");
const [migrationMessage, setMigrationMessage] = useState("");
+
+ // Register Queries state
+ const [registerGraph, setRegisterGraph] = useState("");
+ const [registerMode, setRegisterMode] = useState<"single" | "multiple">("single");
+ const [registeredQueries, setRegisteredQueries] = useState([]);
+ const [installedQueries, setInstalledQueries] = useState([]);
+ const [queryDrafts, setQueryDrafts] = useState([emptyQueryDraft()]);
+ const [registerLoading, setRegisterLoading] = useState(false);
+ const [registerSaving, setRegisterSaving] = useState(false);
+ const [registerMessage, setRegisterMessage] = useState("");
+ const [queryListFilter, setQueryListFilter] = useState("");
+ const [registerPage, setRegisterPage] = useState<"registered" | "original">("registered");
+ const registerStatusRef = useRef(null);
// Reset states when dialogs close
const handleInitializeDialogChange = (open: boolean) => {
if (!open && isConfirmDialogOpen) {
@@ -154,27 +191,14 @@ const KGAdmin = () => {
return;
}
setMigrationStatus(data);
- if (
- !data.needs_repair &&
- !data.embeddings_incomplete &&
- !data.community_summaries_incomplete
- ) {
+ if (!data.needs_repair) {
setMigrationMessage("✅ Graph is up to date — no repairs needed.");
} else {
- const parts: string[] = [];
const out = data.queries?.outdated?.length || 0;
const miss = data.queries?.not_installed?.length || 0;
- if (out || miss)
- parts.push(`${out} outdated query(s), ${miss} not installed`);
- if (data.embeddings_incomplete)
- parts.push(
- `${data.embeddings?.total_missing ?? 0} vertices missing embeddings`
- );
- if (data.community_summaries_incomplete)
- parts.push(
- `${data.community_summaries?.needs_resummarize ?? 0} communities need re-summarization`
- );
- setMigrationMessage(`Found: ${parts.join("; ")}.`);
+ setMigrationMessage(
+ `Found ${out} outdated query(s) and ${miss} not installed.`
+ );
}
} catch (err: any) {
setMigrationMessage(`Check failed: ${err.message || err}`);
@@ -248,46 +272,213 @@ const KGAdmin = () => {
}
};
- // Targeted data-integrity regeneration (not a full rebuild). action is
- // "regenerate_embeddings" or "regenerate_summaries".
- const runRegenerate = async (
- action: "regenerate_embeddings" | "regenerate_summaries"
- ) => {
- const auth = sessionStorage.getItem("auth");
- if (!auth) {
- setMigrationMessage("Not authenticated.");
+ const loadRegisterQueries = async (graph: string, keepMessage = false) => {
+ if (!graph.trim()) return;
+ const creds = sessionStorage.getItem("auth");
+ if (!creds) {
+ setRegisterMessage("Not authenticated.");
+ return;
+ }
+ setRegisterLoading(true);
+ setRegisteredQueries([]);
+ setInstalledQueries([]);
+ try {
+ const registeredResp = await fetch(`/ui/${graph}/registered_queries`, {
+ headers: { Authorization: creds },
+ });
+ const registeredData = await registeredResp.json();
+ if (!registeredResp.ok) {
+ setRegisterMessage(
+ registeredData?.detail || `Failed to list queries: ${registeredResp.statusText}`
+ );
+ return;
+ }
+ setRegisteredQueries(registeredData?.registered || registeredData?.queries || []);
+ setInstalledQueries(registeredData?.installed || []);
+ if (!keepMessage) setRegisterMessage("");
+ } catch (err: any) {
+ setRegisterMessage(`Failed to load queries: ${err.message || err}`);
+ } finally {
+ setRegisterLoading(false);
+ }
+ };
+
+ const openRegisterDialog = () => {
+ setRegisterMessage("");
+ setQueryListFilter("");
+ setRegisterPage("registered");
+ setRegisterMode("single");
+ setQueryDrafts([emptyQueryDraft()]);
+ const initial =
+ sessionStorage.getItem("selectedGraph") || availableGraphs[0] || "";
+ setRegisterGraph(initial);
+ setRegisterDialogOpen(true);
+ if (initial) loadRegisterQueries(initial);
+ };
+
+ const useInstalledQuery = (q: ListedQuery) => {
+ setRegisterMode("single");
+ setQueryDrafts([
+ {
+ name: q.function_header,
+ returns: "",
+ useFor: "",
+ doNotUse: "",
+ gsql: "",
+ },
+ ]);
+ setRegisterPage("registered");
+ setRegisterMessage(
+ `Selected installed query "${q.function_header}". Fill in what it returns and when to use it, then click Register.`
+ );
+ };
+
+ const visibleDrafts = registerMode === "single" ? queryDrafts.slice(0, 1) : queryDrafts;
+ const queryFilter = queryListFilter.trim().toLowerCase();
+ const matchesQueryFilter = (q: ListedQuery) =>
+ !queryFilter ||
+ q.function_header.toLowerCase().includes(queryFilter) ||
+ (q.description || "").toLowerCase().includes(queryFilter);
+ const visibleRegisteredQueries = registeredQueries.filter(matchesQueryFilter);
+ const visibleOriginalQueries = installedQueries.filter(matchesQueryFilter);
+
+ const updateDraft = (index: number, patch: Partial) => {
+ setQueryDrafts((prev) => {
+ const next = prev.length ? [...prev] : [emptyQueryDraft()];
+ while (next.length <= index) next.push(emptyQueryDraft());
+ next[index] = { ...next[index], ...patch };
+ return next;
+ });
+ };
+
+ const runRegisterQueries = async () => {
+ if (!registerGraph) {
+ setRegisterMessage("Pick a graph first.");
+ return;
+ }
+ const drafts = visibleDrafts
+ .map((d) => ({
+ name: d.name.trim(),
+ returns: d.returns.trim(),
+ useFor: d.useFor.trim(),
+ doNotUse: d.doNotUse.trim(),
+ gsql: (d.gsql || "").trim(),
+ }))
+ .filter((d) => d.name || d.returns || d.useFor || d.gsql);
+ if (drafts.length === 0) {
+ setRegisterMessage(
+ "Enter a query name, what it returns, and when to use it (paste GSQL only if the query is not installed yet)."
+ );
+ return;
+ }
+ if (drafts.some((d) => !d.name)) {
+ setRegisterMessage("Each query needs a name.");
+ return;
+ }
+ const missingReturns = drafts.filter((d) => !d.returns).map((d) => d.name);
+ if (missingReturns.length > 0) {
+ setRegisterMessage(`Say what this query returns for: ${missingReturns.join(", ")}`);
+ return;
+ }
+ const missingUse = drafts.filter((d) => !d.useFor).map((d) => d.name);
+ if (missingUse.length > 0) {
+ setRegisterMessage(`Say when to use this tool for: ${missingUse.join(", ")}`);
+ return;
+ }
+ const creds = sessionStorage.getItem("auth");
+ if (!creds) {
+ setRegisterMessage("Not authenticated.");
return;
}
- const isEmb = action === "regenerate_embeddings";
- setMigrationRegenerating(action);
- setMigrationMessage(
- isEmb ? "Regenerating embeddings…" : "Regenerating community summaries…"
+ const willCreate = drafts.some((d) => d.gsql);
+ setRegisterSaving(true);
+ setRegisterMessage(
+ willCreate
+ ? drafts.length > 1
+ ? "Creating, installing, and registering queries…"
+ : "Creating, installing, and registering query…"
+ : drafts.length > 1
+ ? "Registering queries…"
+ : "Registering query…"
);
+ requestAnimationFrame(() => {
+ registerStatusRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
+ });
try {
- const resp = await fetch(`/ui/${migrationGraph}/migration/${action}`, {
+ const resp = await fetch(`/ui/${registerGraph}/registered_queries`, {
method: "POST",
- headers: { Authorization: auth },
+ headers: { Authorization: creds, "Content-Type": "application/json" },
+ body: JSON.stringify({
+ queries: drafts.map((d) => ({
+ function_header: d.name,
+ description: buildToolDescription(d.name, d.returns, d.useFor, d.doNotUse),
+ gsql: d.gsql,
+ })),
+ }),
});
- const data = await resp.json().catch(() => ({}));
+ const data = await resp.json();
if (!resp.ok) {
- setMigrationMessage(
- `Regenerate failed: ${data.detail || resp.statusText}`
- );
+ const detail =
+ typeof data?.detail === "string"
+ ? data.detail
+ : data?.detail?.message || JSON.stringify(data?.detail || data);
+ setRegisterMessage(detail || `Register failed: ${resp.statusText}`);
return;
}
- const done = isEmb ? data.regenerated ?? 0 : data.resummarized ?? 0;
- const skipped = data.skipped ?? 0;
- const verb = isEmb ? "Re-embedded" : "Re-summarized";
- setMigrationMessage(
- `✅ ${verb} ${done}` +
- (skipped ? `; ${skipped} skipped (need a rebuild).` : ".")
+ const createdCount = data.created?.length || 0;
+ const registeredCount = data.registered?.length || drafts.length;
+ setRegisterMessage(
+ createdCount > 0
+ ? `✅ Created, installed, and registered ${registeredCount} quer${
+ registeredCount === 1 ? "y" : "ies"
+ }. GraphRAG tagged the GSQL description so the agent can use ${
+ registeredCount === 1 ? "it" : "them"
+ } as a tool.`
+ : `✅ Registered ${registeredCount} quer${
+ registeredCount === 1 ? "y" : "ies"
+ }. GraphRAG tagged the GSQL description.`
);
- // Refresh so the counts reflect the regenerated state.
- await runMigrationCheck(migrationGraph);
+ setQueryDrafts([emptyQueryDraft()]);
+ await loadRegisterQueries(registerGraph, true);
+ } catch (err: any) {
+ setRegisterMessage(`Register failed: ${err.message || err}`);
+ } finally {
+ setRegisterSaving(false);
+ }
+ };
+
+ const runUnregisterQuery = async (header: string) => {
+ const ok = await confirm(
+ `Unregister query "${header}"? This removes it from GraphRAG candidates. The installed GSQL query on the graph is not dropped.`
+ );
+ if (!ok) return;
+ const creds = sessionStorage.getItem("auth");
+ if (!creds) {
+ setRegisterMessage("Not authenticated.");
+ return;
+ }
+ setRegisterSaving(true);
+ setRegisterMessage(`Unregistering ${header}…`);
+ requestAnimationFrame(() => {
+ registerStatusRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
+ });
+ try {
+ const resp = await fetch(`/ui/${registerGraph}/registered_queries/delete`, {
+ method: "POST",
+ headers: { Authorization: creds, "Content-Type": "application/json" },
+ body: JSON.stringify({ ids: [header] }),
+ });
+ const data = await resp.json();
+ if (!resp.ok) {
+ setRegisterMessage(data?.detail || `Delete failed: ${resp.statusText}`);
+ return;
+ }
+ setRegisterMessage(`✅ Unregistered ${header}.`);
+ await loadRegisterQueries(registerGraph, true);
} catch (err: any) {
- setMigrationMessage(`Regenerate failed: ${err.message || err}`);
+ setRegisterMessage(`Delete failed: ${err.message || err}`);
} finally {
- setMigrationRegenerating("");
+ setRegisterSaving(false);
}
};
@@ -1478,6 +1669,30 @@ const KGAdmin = () => {
+ {/* Register Queries Card */}
+
+
+
+
+
+
+ Register Queries
+
+
+ Paste GSQL to create and install a query, then register it as a GraphRAG tool. Leave GSQL empty to tag an already-installed query.
+