diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py index fe77ac1..38022ac 100644 --- a/common/llm_services/base_llm.py +++ b/common/llm_services/base_llm.py @@ -1013,7 +1013,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 +1038,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 +1071,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 +1094,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 clauses, assign each clause to its own retrieval step. If another clause still needs passages or typed graph facts after one is covered, plan hybrid/community/structural for that clause too. +- 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 +1212,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 context. Use structured-query results for typed graph facts; use document passages for "what does the report/document say." Do not quote a structured note as what a named report says. If retrieved passages are off-topic for a part, say that part is not in the retrieved documents. - **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. +

+
+
+ +
+
+ {/* Initialize Dialog */} @@ -2953,97 +3168,6 @@ const KGAdmin = () => { )} - - {/* Data-integrity health: embedding coverage */} - {migrationStatus.embeddings && - Object.keys(migrationStatus.embeddings.by_type).length > 0 && ( -
-
-
- Embedding health - {migrationStatus.embeddings.total_missing > 0 - ? ` — ${migrationStatus.embeddings.total_missing} missing` - : " — all embedded"} -
- {migrationStatus.embeddings_incomplete && ( - - )} -
-
- {Object.entries(migrationStatus.embeddings.by_type) - .map( - ([t, c]) => `${t}: ${c.missing}/${c.total} missing` - ) - .join(" · ")} -
-
- )} - - {/* Data-integrity health: community summaries */} - {migrationStatus.community_summaries && - (migrationStatus.community_summaries.total ?? 0) > 0 && ( -
-
-
- Community summaries —{" "} - {migrationStatus.community_summaries.needs_resummarize}/ - {migrationStatus.community_summaries.total} need - re-summarization -
- {migrationStatus.community_summaries_incomplete && ( - - )} -
-
- )} )} @@ -3095,6 +3219,450 @@ const KGAdmin = () => { + + {/* Register Queries Dialog */} + + e.preventDefault()} + > + + + Register Queries + + + Register installed GSQL as GraphRAG tools, or pick an original query already on the graph. + + + +
+
+ + +
+ + { + if (registerSaving) return; + setQueryListFilter(""); + setRegisterPage(value as "registered" | "original"); + }} + className="w-full" + > + + + + Registered Queries + + + + Original Queries + + + + {registerLoading && ( +
+ + Loading queries… +
+ )} + + +

+ Fill what the query returns and when to use it — GraphRAG merges those fields into the same one-line description the agent already uses for hybrid search and structural retrieve. Leave GSQL empty to tag an already-installed query. +

+ {!registerLoading && registerGraph && ( +
+ setQueryListFilter(e.target.value)} + placeholder="Filter registered queries by name…" + disabled={registerSaving} + className="dark:border-[#3D3D3D] dark:bg-shadeA dark:text-white" + /> + +
+
+ Registered queries ({registeredQueries.length}) +
+

+ Tagged with [GRAPHRAG_TOOL]. The agent can pick these as tools. +

+ {registeredQueries.length === 0 ? ( +

+ None registered yet. Paste GSQL below, or open Original Queries and click Use. +

+ ) : visibleRegisteredQueries.length === 0 ? ( +

+ No registered queries match the filter. +

+ ) : ( +
+ {visibleRegisteredQueries.map((q) => ( +
+
+
+
+ {q.function_header} +
+ + Registered + +
+
+ {q.description || "No description"} +
+
+ +
+ ))} +
+ )} +
+
+ )} + +
+ +
+ + +
+
+ +
+ {visibleDrafts.map((draft, index) => ( +
+ {registerMode === "multiple" && ( +
+
+ Query {index + 1} +
+ {visibleDrafts.length > 1 && ( + + )} +
+ )} +
+ + updateDraft(index, { name: e.target.value })} + placeholder="e.g. my_custom_query" + disabled={registerSaving} + className="dark:border-[#3D3D3D] dark:bg-shadeA dark:text-white" + /> +
+
+ +