From b7c8877f5efc7e983a84373919641f4f511e8bf5 Mon Sep 17 00:00:00 2001 From: Chengbiao Jin Date: Tue, 25 Nov 2025 17:00:45 -0800 Subject: [PATCH 1/2] GML-2019 handle more table view display content --- common/llm_services/base_llm.py | 5 +- .../src/components/CustomChatMessage.tsx | 4 +- graphrag-ui/src/components/Interact.tsx | 22 ++- .../components/tables/KnowledgeTablePro.tsx | 141 ++++++++++++++++-- graphrag/app/agent/agent_graph.py | 30 ++-- 5 files changed, 176 insertions(+), 26 deletions(-) diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py index 84f6b80..93ec0cf 100644 --- a/common/llm_services/base_llm.py +++ b/common/llm_services/base_llm.py @@ -107,10 +107,11 @@ def generate_gsql_prompt(self): def route_response_prompt(self): """Property to get the prompt for the RouteResponse tool.""" prompt = """\ -You are an expert at routing a user question to a vectorstore, function calls, or purely conversation history. -Use the conversation history for questions that are directly related to the conversation history. +You are an expert at routing a user question to a vectorstore, function calls, or conversation history. +Use the conversation history for questions that are similar to previous ones or that reference earlier answers or responses. Use the vectorstore for questions on that would be best suited by text documents. Use the function calls for questions that ask about structured data, or operations on structured data. +Questions referring to same entities in a previous, earlier, or above answer or response should be routed to the conversation history. Keep in mind that some questions about documents such as "how many documents are there?" can be answered by function calls. The function calls can be used to answer questions about these entities: {v_types} and relationships: {e_types}. Otherwise, use vectorstore. Choose one of 'functions', 'vectorstore', or 'history' based on the question and conversation history. diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx index 9119e9a..9c2c5ee 100755 --- a/graphrag-ui/src/components/CustomChatMessage.tsx +++ b/graphrag-ui/src/components/CustomChatMessage.tsx @@ -149,7 +149,7 @@ export const CustomChatMessage: FC = ({ }; const handleShowTable = () => { - if (!message.query_sources?.result && !message.query_sources?.answer) { + if (message.response_type == 'history' || !message.query_sources?.result) { return false; } setShowTableVis(prev => !prev); @@ -225,8 +225,6 @@ export const CustomChatMessage: FC = ({
{message.query_sources?.result ? ( - ) : message.query_sources?.answer ? ( - ) : (
No table data available diff --git a/graphrag-ui/src/components/Interact.tsx b/graphrag-ui/src/components/Interact.tsx index f7aea6a..3d13a61 100644 --- a/graphrag-ui/src/components/Interact.tsx +++ b/graphrag-ui/src/components/Interact.tsx @@ -98,11 +98,29 @@ export const Interactions: FC = ({ Explain
-
showGraph()}> +
{ + if (message.query_sources?.result?.edges) { + showGraph(); + } + }} + >
-
showTable()}> +
{ + if (message.query_sources?.result) { + showTable(); + } + }} + >
diff --git a/graphrag-ui/src/components/tables/KnowledgeTablePro.tsx b/graphrag-ui/src/components/tables/KnowledgeTablePro.tsx index 32dbd33..a6febbb 100644 --- a/graphrag-ui/src/components/tables/KnowledgeTablePro.tsx +++ b/graphrag-ui/src/components/tables/KnowledgeTablePro.tsx @@ -65,18 +65,57 @@ export const KnowledgeTablPro = ({ data }) => { return; } - // Handle string data + // Handle string data - display it directly if (typeof data === 'string') { setDataType('gsql'); - setTableData([]); - setColumns([]); + setTableData([{ content: data }]); + setColumns(['content']); setEdges([]); return; } // Handle Cypher query results (answer field) - these are objects with data arrays if (typeof data === 'object' && !Array.isArray(data)) { - // Check if this is Cypher data (object with array values like {"T": [...]}) + // Check if this is final_retrieval data structure (SupportAI format) + if (data.final_retrieval && typeof data.final_retrieval === 'object') { + setDataType('final_retrieval'); + setEdges([]); + + // Convert final_retrieval structure to table rows + // Format: {chunk1: [text1, text2, ...], chunk2: [text3, text4, ...]} + // One row per chunk with all texts combined + const tableRows: any[] = []; + const finalRetrieval = data.final_retrieval; + + Object.keys(finalRetrieval).forEach((chunkKey) => { + const texts = finalRetrieval[chunkKey]; + if (Array.isArray(texts)) { + // Combine all texts for this chunk into a single string + const combinedText = texts.join('\n\n'); + tableRows.push({ + chunk: chunkKey, + text: combinedText + }); + } else if (typeof texts === 'string') { + // Handle case where value is a single string instead of array + tableRows.push({ + chunk: chunkKey, + text: texts + }); + } + }); + + if (tableRows.length > 0) { + setColumns(['chunk', 'text']); + setTableData(tableRows); + } else { + setColumns([]); + setTableData([]); + } + return; // Exit early for final_retrieval data + } + + // Check if this is Cypher data (object with array values like {"T": [...]} or object values like {"T": {...}}) const dictKeys = Object.keys(data); if (dictKeys.length > 0) { const firstKey = dictKeys[0]; @@ -91,23 +130,42 @@ export const KnowledgeTablPro = ({ data }) => { const cols = Object.keys(firstRow); setColumns(cols); setTableData(firstValue); - } else { + } else { + setColumns([]); + setTableData([]); + } + return; // Exit early for Cypher data + } else if (firstValue && typeof firstValue === 'object' && !Array.isArray(firstValue)) { + // Handle object values like {"T": {"entity_type_count": 1036}} + // Convert to table with Key and Value columns + setDataType('cypher'); + setEdges([]); // Clear GSQL edges for Cypher data + + const tableRows = Object.keys(firstValue).map(key => ({ + Key: key, + Value: firstValue[key] + })); + + if (tableRows.length > 0) { + setColumns(['Key', 'Value']); + setTableData(tableRows); + } else { setColumns([]); setTableData([]); } return; // Exit early for Cypher data } } - + // Handle GSQL results (object with @@edges) setDataType('gsql'); setTableData([]); setColumns([]); setEdges([]); - + // Look for @@edges in the data let setresults: any[] | null = null; - + // Check if data is an array and look for @@edges in each item if (Array.isArray(data)) { for (const item of data) { @@ -120,7 +178,7 @@ export const KnowledgeTablPro = ({ data }) => { // Direct @@edges in the object setresults = data['@@edges']; } - + if (setresults && Array.isArray(setresults)) { setEdges(setresults); @@ -172,7 +230,70 @@ export const KnowledgeTablPro = ({ data }) => { return ( <> - {dataType === 'cypher' && tableData.length > 0 ? ( + {dataType === 'final_retrieval' && tableData.length > 0 ? ( + <> + + + + Chunk + Text + + + + {tableData.slice(startIndex, endIndex).map((item: any, index: number) => ( + + {item.chunk} + {item.text} + + ))} + +
+ {tableData.length > rowsPerPage && ( + + + + { + setStartIndex(startIndex - rowsPerPage); + setEndIndex(endIndex - rowsPerPage); + }} /> + + + + = tableData.length ? "pointer-events-none opacity-50" : undefined + } + onClick={() => { + setStartIndex(startIndex + rowsPerPage); + setEndIndex(endIndex + rowsPerPage); + }} /> + + + + )} + + ) : dataType === 'gsql' && tableData.length > 0 && edges.length === 0 ? ( + <> + + + + Content + + + + + + {tableData[0].content} + + + +
+ + ) : dataType === 'cypher' && tableData.length > 0 ? ( <> diff --git a/graphrag/app/agent/agent_graph.py b/graphrag/app/agent/agent_graph.py index 2eefa2b..b3c6c4e 100644 --- a/graphrag/app/agent/agent_graph.py +++ b/graphrag/app/agent/agent_graph.py @@ -180,7 +180,7 @@ def generate_cypher(self, state): """ Run the agent cypher generator. """ - self.emit_progress("Generating the Cypher to answer your question") + self.emit_progress("Generating the query to answer your question") gen_history = [] response_json = None @@ -196,9 +196,9 @@ def generate_cypher(self, state): break except Exception as e: gen_history.append(f"{i}: {cypher}\n\tError: {json_str}\n") - if response_json: + if response_json and not self.is_query_result_empty(response_json["results"][0]): state["context"] = { - "answer": response_json["results"][0], + "result": response_json["results"][0], "cypher": cypher, "reasoning": "The following OpenCypher query was executed to answer the question. {}".format( cypher @@ -208,7 +208,7 @@ def generate_cypher(self, state): state["context"] = { "error": True, "cypher": cypher, - "answer": json_str + "result": json_str } if "error_history" not in state or state["error_history"] is None: state["error_history"] = [] @@ -397,9 +397,9 @@ def generate_answer(self, state): elif state["lookup_source"] == "cypher": logger.debug_pii( - f"""request_id={req_id_cv.get()} Got result: {state["context"]["answer"]}""" + f"""request_id={req_id_cv.get()} Got result: {state["context"]["result"]}""" ) - answer = step.generate_answer(state["question"], state["context"]["answer"], state["context"]["cypher"]) + answer = step.generate_answer(state["question"], state["context"]["result"], state["context"]["cypher"]) elif state["lookup_source"] == "history": state["context"] = { @@ -444,7 +444,6 @@ def generate_answer(self, state): return state - def replace_s3_urls_with_presigned(self, content, expires_in=3600): """ Recursively detects S3 URLs in content (string, list, or dict) @@ -485,7 +484,6 @@ def process(value): return value return process(content) - def convert_image_refs_to_markdown(self, text): """ @@ -530,7 +528,6 @@ def convert_image_refs_to_markdown(self, text): else: return text - def rewrite_question(self, state): """ Run the agent question rewriter. @@ -541,6 +538,21 @@ def rewrite_question(self, state): state["question"] = step.rewrite_question(question_str) return state + def is_query_result_empty(self, query_result) -> bool: + """ + Check if the query result is empty or contains empty values. + """ + if not query_result: + return True + + if isinstance(query_result, list): + return all(self.is_query_result_empty(item) for item in query_result) + + if isinstance(query_result, dict): + return all(self.is_query_result_empty(v) for v in query_result.values()) + + return False + # remove halucinaton check, always return grounded def check_answer_for_hallucinations(self, state): """ From 5add1b23675b9c48937b9b27847c03b7e9640935 Mon Sep 17 00:00:00 2001 From: Chengbiao Jin Date: Tue, 25 Nov 2025 17:44:02 -0800 Subject: [PATCH 2/2] rescope empty value --- graphrag/app/agent/agent_graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphrag/app/agent/agent_graph.py b/graphrag/app/agent/agent_graph.py index b3c6c4e..3320c78 100644 --- a/graphrag/app/agent/agent_graph.py +++ b/graphrag/app/agent/agent_graph.py @@ -542,10 +542,10 @@ def is_query_result_empty(self, query_result) -> bool: """ Check if the query result is empty or contains empty values. """ - if not query_result: + if query_result in ("", [], {}, (), set(), range(0), None): return True - if isinstance(query_result, list): + if isinstance(query_result, (list, set)): return all(self.is_query_result_empty(item) for item in query_result) if isinstance(query_result, dict):