Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions common/llm_services/base_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions graphrag-ui/src/components/CustomChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export const CustomChatMessage: FC<IChatbotMessageProps> = ({
};

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);
Expand Down Expand Up @@ -225,8 +225,6 @@ export const CustomChatMessage: FC<IChatbotMessageProps> = ({
<div className="relative w-full h-[550px] my-10 border border-solid border-[#000] my-10 h-auto">
{message.query_sources?.result ? (
<KnowledgeTablPro data={message.query_sources?.result} />
) : message.query_sources?.answer ? (
<KnowledgeTablPro data={message.query_sources?.answer} />
) : (
<div className="flex items-center justify-center h-full text-gray-500">
No table data available
Expand Down
22 changes: 20 additions & 2 deletions graphrag-ui/src/components/Interact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,29 @@ export const Interactions: FC<Interactions> = ({
<span className="text-xs">Explain</span>
</div>

<div className="w-[28px] h-[28px] bg-shadeA flex items-center justify-center rounded-sm ml-5 mr-1 cursor-pointer" onClick={() => showGraph()}>
<div
className={`w-[28px] h-[28px] bg-shadeA flex items-center justify-center rounded-sm ml-5 mr-1 ${
message.query_sources?.result?.edges ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'
}`}
onClick={() => {
if (message.query_sources?.result?.edges) {
showGraph();
}
}}
>
<PiGraph className="text-[15px]" />
</div>

<div className="w-[28px] h-[28px] bg-shadeA flex items-center justify-center rounded-sm mr-1 cursor-pointer" onClick={() => showTable()}>
<div
className={`w-[28px] h-[28px] bg-shadeA flex items-center justify-center rounded-sm mr-1 ${
message.query_sources?.result ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'
}`}
onClick={() => {
if (message.query_sources?.result) {
showTable();
}
}}
>
<FaTable className="text-[15px]" />
</div>

Expand Down
141 changes: 131 additions & 10 deletions graphrag-ui/src/components/tables/KnowledgeTablePro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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) {
Expand All @@ -120,7 +178,7 @@ export const KnowledgeTablPro = ({ data }) => {
// Direct @@edges in the object
setresults = data['@@edges'];
}

if (setresults && Array.isArray(setresults)) {
setEdges(setresults);

Expand Down Expand Up @@ -172,7 +230,70 @@ export const KnowledgeTablPro = ({ data }) => {

return (
<>
{dataType === 'cypher' && tableData.length > 0 ? (
{dataType === 'final_retrieval' && tableData.length > 0 ? (
<>
<Table className="text-[11px]">
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[11px]">Chunk</TableHead>
<TableHead className="text-[11px]">Text</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tableData.slice(startIndex, endIndex).map((item: any, index: number) => (
<TableRow key={index}>
<TableCell className="text-left text-[11px]">{item.chunk}</TableCell>
<TableCell className="text-left text-[11px] whitespace-pre-wrap">{item.text}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{tableData.length > rowsPerPage && (
<Pagination className="text-[11px]">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className={
startIndex === 0 ? "pointer-events-none opacity-50" : undefined
}
onClick={() => {
setStartIndex(startIndex - rowsPerPage);
setEndIndex(endIndex - rowsPerPage);
}} />
</PaginationItem>

<PaginationItem>
<PaginationNext
className={
endIndex >= tableData.length ? "pointer-events-none opacity-50" : undefined
}
onClick={() => {
setStartIndex(startIndex + rowsPerPage);
setEndIndex(endIndex + rowsPerPage);
}} />
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</>
) : dataType === 'gsql' && tableData.length > 0 && edges.length === 0 ? (
<>
<Table className="text-[11px]">
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[11px]">Content</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="text-left text-[11px] whitespace-pre-wrap">
{tableData[0].content}
</TableCell>
</TableRow>
</TableBody>
</Table>
</>
) : dataType === 'cypher' && tableData.length > 0 ? (
<>
<Table className="text-[11px]">
<TableHeader>
Expand Down
30 changes: 21 additions & 9 deletions graphrag/app/agent/agent_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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"] = []
Expand Down Expand Up @@ -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"] = {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -485,7 +484,6 @@ def process(value):
return value

return process(content)


def convert_image_refs_to_markdown(self, text):
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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 query_result in ("", [], {}, (), set(), range(0), None):
return True

if isinstance(query_result, (list, set)):
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):
"""
Expand Down
Loading