gauthamnairy commited on
Commit
0628c4c
·
verified ·
1 Parent(s): ced14f2

Update pageindex/core/tree_index.py

Browse files
Files changed (1) hide show
  1. pageindex/core/tree_index.py +226 -226
pageindex/core/tree_index.py CHANGED
@@ -1,226 +1,226 @@
1
- import json
2
- import re
3
- import asyncio
4
- from typing import List, Dict, Any, Optional
5
-
6
- # Import potentially available util functions or re-implement if necessary
7
- # Assuming we are in pageindex/core/tree_index.py, and page_index_md is in pageindex/page_index_md.py
8
- try:
9
- from ...pageindex.page_index_md import (
10
- extract_nodes_from_markdown,
11
- extract_node_text_content,
12
- build_tree_from_nodes,
13
- count_tokens # Assuming this exists in utils or page_index_md imports
14
- )
15
- except ImportError:
16
- # If imports fail due to path issues, we might need to adjust python path or copy logic.
17
- # For now, let's assume we can import from the sibling directory if the package structure is correct.
18
- # Alternatively, we can re-implement the core logic here to be self-contained.
19
- pass
20
-
21
- class TreeIndex:
22
- def __init__(self, model: str = "gpt-4-turbo"):
23
- self.model = model
24
- self.tree = None
25
-
26
- def build_from_markdown(self, markdown_text: str) -> str:
27
- """
28
- Builds the tree index from markdown text.
29
-
30
- Args:
31
- markdown_text (str): The markdown content.
32
-
33
- Returns:
34
- str: A document ID or confirmation (for this implementation, it returns 'doc_id').
35
- """
36
- print(f"Extracting nodes from markdown content...")
37
- # Re-implementing core logic here to ensure stability and avoid complex relative imports for now
38
- # This matches page_index_md.py logic
39
-
40
- # 1. Extract nodes
41
- node_list, lines = self._extract_nodes(markdown_text)
42
-
43
- # 2. Extract text content
44
- nodes_with_content = self._extract_node_text(node_list, lines)
45
-
46
- # 3. Build tree
47
- self.tree = self._build_tree(nodes_with_content)
48
-
49
- # 4. Add Node IDs (simple counter)
50
- self._add_node_ids(self.tree)
51
-
52
- print(f"Tree built successfully.")
53
- return "doc_1" # In a real app this would be a UUID
54
-
55
- def _extract_nodes(self, markdown_content):
56
- header_pattern = r'^(#{1,6})\s+(.+)$'
57
- code_block_pattern = r'^```'
58
- node_list = []
59
-
60
- lines = markdown_content.split('\n')
61
- in_code_block = False
62
-
63
- for line_num, line in enumerate(lines, 1):
64
- stripped_line = line.strip()
65
-
66
- if re.match(code_block_pattern, stripped_line):
67
- in_code_block = not in_code_block
68
- continue
69
-
70
- if not stripped_line:
71
- continue
72
-
73
- if not in_code_block:
74
- match = re.match(header_pattern, stripped_line)
75
- if match:
76
- title = match.group(2).strip()
77
- node_list.append({'node_title': title, 'line_num': line_num})
78
-
79
- return node_list, lines
80
-
81
- def _extract_node_text(self, node_list, lines):
82
- all_nodes = []
83
- for node in node_list:
84
- line_content = lines[node['line_num'] - 1]
85
- header_match = re.match(r'^(#{1,6})', line_content)
86
-
87
- if header_match:
88
- all_nodes.append({
89
- 'title': node['node_title'],
90
- 'line_num': node['line_num'],
91
- 'level': len(header_match.group(1))
92
- })
93
-
94
- for i, node in enumerate(all_nodes):
95
- start_line = node['line_num'] - 1
96
- if i + 1 < len(all_nodes):
97
- end_line = all_nodes[i + 1]['line_num'] - 1
98
- else:
99
- end_line = len(lines)
100
-
101
- node['text'] = '\n'.join(lines[start_line:end_line]).strip()
102
- return all_nodes
103
-
104
- def _build_tree(self, node_list):
105
- if not node_list:
106
- return []
107
-
108
- stack = []
109
- root_nodes = []
110
-
111
- for node in node_list:
112
- current_level = node['level']
113
-
114
- tree_node = {
115
- 'title': node['title'],
116
- 'text': node['text'],
117
- 'line_num': node['line_num'],
118
- 'nodes': []
119
- }
120
-
121
- while stack and stack[-1][1] >= current_level:
122
- stack.pop()
123
-
124
- if not stack:
125
- root_nodes.append(tree_node)
126
- else:
127
- parent_node, parent_level = stack[-1]
128
- parent_node['nodes'].append(tree_node)
129
-
130
- stack.append((tree_node, current_level))
131
-
132
- return root_nodes
133
-
134
- def _add_node_ids(self, nodes, prefix=""):
135
- for i, node in enumerate(nodes):
136
- node_id = f"{prefix}{i+1}"
137
- node['node_id'] = node_id
138
- if node.get('nodes'):
139
- self._add_node_ids(node['nodes'], prefix=f"{node_id}.")
140
-
141
- def reasoning_search(self, query: str, llm_client: Any) -> str:
142
- """
143
- Performs a tree search to find relevant nodes for the query.
144
-
145
- Args:
146
- query (str): The user query.
147
- llm_client: The initialized OpenAI client.
148
-
149
- Returns:
150
- str: Context concatenated from relevant nodes.
151
- """
152
- if not self.tree:
153
- return "Tree not index built. Please upload a document first."
154
-
155
- # Simplify tree for prompt (remove raw text to save tokens, keep titles and IDs)
156
- tree_summary = self._get_tree_structure_summary(self.tree)
157
-
158
- prompt = f"""
159
- You are given a query and the tree structure of a document.
160
- You need to find all nodes that are likely to contain the answer.
161
-
162
- Query: {query}
163
-
164
- Document tree structure:
165
- {json.dumps(tree_summary, indent=2)}
166
-
167
- Reply in the following JSON format:
168
- {{
169
- "thinking": <your reasoning about which nodes are relevant>,
170
- "node_list": ["node_id1", "node_id2", ...]
171
- }}
172
- """
173
-
174
- try:
175
- response = llm_client.chat.completions.create(
176
- model="gpt-4-turbo", # Provide a default, though the client might override or ignored it for NIM
177
- messages=[{"role": "user", "content": prompt}],
178
- temperature=0.1
179
- )
180
-
181
- content = response.choices[0].message.content
182
- # Basic JSON extraction if LLM adds markdown
183
- if "```json" in content:
184
- content = content.split("```json")[1].split("```")[0].strip()
185
- elif "```" in content:
186
- content = content.split("```")[1].split("```")[0].strip()
187
-
188
- result = json.loads(content)
189
- relevant_node_ids = result.get("node_list", [])
190
-
191
- # Retrieve text for these nodes
192
- context_parts = []
193
- for node_id in relevant_node_ids:
194
- node = self._find_node_by_id(self.tree, node_id)
195
- if node:
196
- context_parts.append(f"--- Section: {node['title']} (ID: {node_id}) ---\n{node['text']}\n")
197
-
198
- full_context = "\n".join(context_parts)
199
- if not full_context:
200
- return "No relevant context found in the document tree."
201
- return full_context
202
-
203
- except Exception as e:
204
- return f"Error during reasoning search: {str(e)}"
205
-
206
- def _get_tree_structure_summary(self, nodes):
207
- summary = []
208
- for node in nodes:
209
- node_summary = {
210
- 'title': node['title'],
211
- 'node_id': node['node_id']
212
- }
213
- if node.get('nodes'):
214
- node_summary['nodes'] = self._get_tree_structure_summary(node['nodes'])
215
- summary.append(node_summary)
216
- return summary
217
-
218
- def _find_node_by_id(self, nodes, target_id):
219
- for node in nodes:
220
- if node.get('node_id') == target_id:
221
- return node
222
- if node.get('nodes'):
223
- found = self._find_node_by_id(node['nodes'], target_id)
224
- if found:
225
- return found
226
- return None
 
1
+ import json
2
+ import re
3
+ import asyncio
4
+ from typing import List, Dict, Any, Optional
5
+
6
+ # Import potentially available util functions or re-implement if necessary
7
+ # Assuming we are in pageindex/core/tree_index.py, and page_index_md is in pageindex/page_index_md.py
8
+ try:
9
+ from ...pageindex.page_index_md import (
10
+ extract_nodes_from_markdown,
11
+ extract_node_text_content,
12
+ build_tree_from_nodes,
13
+ count_tokens # Assuming this exists in utils or page_index_md imports
14
+ )
15
+ except ImportError:
16
+ # If imports fail due to path issues, we might need to adjust python path or copy logic.
17
+ # For now, let's assume we can import from the sibling directory if the package structure is correct.
18
+ # Alternatively, we can re-implement the core logic here to be self-contained.
19
+ pass
20
+
21
+ class TreeIndex:
22
+ def __init__(self, model: str = "gpt-4-turbo"):
23
+ self.model = model
24
+ self.tree = None
25
+
26
+ def build_from_markdown(self, markdown_text: str) -> str:
27
+ """
28
+ Builds the tree index from markdown text.
29
+
30
+ Args:
31
+ markdown_text (str): The markdown content.
32
+
33
+ Returns:
34
+ str: A document ID or confirmation (for this implementation, it returns 'doc_id').
35
+ """
36
+ print(f"Extracting nodes from markdown content...")
37
+ # Re-implementing core logic here to ensure stability and avoid complex relative imports for now
38
+ # This matches page_index_md.py logic
39
+
40
+ # 1. Extract nodes
41
+ node_list, lines = self._extract_nodes(markdown_text)
42
+
43
+ # 2. Extract text content
44
+ nodes_with_content = self._extract_node_text(node_list, lines)
45
+
46
+ # 3. Build tree
47
+ self.tree = self._build_tree(nodes_with_content)
48
+
49
+ # 4. Add Node IDs (simple counter)
50
+ self._add_node_ids(self.tree)
51
+
52
+ print(f"Tree built successfully.")
53
+ return "doc_1" # In a real app this would be a UUID
54
+
55
+ def _extract_nodes(self, markdown_content):
56
+ header_pattern = r'^(#{1,6})\s+(.+)$'
57
+ code_block_pattern = r'^```'
58
+ node_list = []
59
+
60
+ lines = markdown_content.split('\n')
61
+ in_code_block = False
62
+
63
+ for line_num, line in enumerate(lines, 1):
64
+ stripped_line = line.strip()
65
+
66
+ if re.match(code_block_pattern, stripped_line):
67
+ in_code_block = not in_code_block
68
+ continue
69
+
70
+ if not stripped_line:
71
+ continue
72
+
73
+ if not in_code_block:
74
+ match = re.match(header_pattern, stripped_line)
75
+ if match:
76
+ title = match.group(2).strip()
77
+ node_list.append({'node_title': title, 'line_num': line_num})
78
+
79
+ return node_list, lines
80
+
81
+ def _extract_node_text(self, node_list, lines):
82
+ all_nodes = []
83
+ for node in node_list:
84
+ line_content = lines[node['line_num'] - 1]
85
+ header_match = re.match(r'^(#{1,6})', line_content)
86
+
87
+ if header_match:
88
+ all_nodes.append({
89
+ 'title': node['node_title'],
90
+ 'line_num': node['line_num'],
91
+ 'level': len(header_match.group(1))
92
+ })
93
+
94
+ for i, node in enumerate(all_nodes):
95
+ start_line = node['line_num'] - 1
96
+ if i + 1 < len(all_nodes):
97
+ end_line = all_nodes[i + 1]['line_num'] - 1
98
+ else:
99
+ end_line = len(lines)
100
+
101
+ node['text'] = '\n'.join(lines[start_line:end_line]).strip()
102
+ return all_nodes
103
+
104
+ def _build_tree(self, node_list):
105
+ if not node_list:
106
+ return []
107
+
108
+ stack = []
109
+ root_nodes = []
110
+
111
+ for node in node_list:
112
+ current_level = node['level']
113
+
114
+ tree_node = {
115
+ 'title': node['title'],
116
+ 'text': node['text'],
117
+ 'line_num': node['line_num'],
118
+ 'nodes': []
119
+ }
120
+
121
+ while stack and stack[-1][1] >= current_level:
122
+ stack.pop()
123
+
124
+ if not stack:
125
+ root_nodes.append(tree_node)
126
+ else:
127
+ parent_node, parent_level = stack[-1]
128
+ parent_node['nodes'].append(tree_node)
129
+
130
+ stack.append((tree_node, current_level))
131
+
132
+ return root_nodes
133
+
134
+ def _add_node_ids(self, nodes, prefix=""):
135
+ for i, node in enumerate(nodes):
136
+ node_id = f"{prefix}{i+1}"
137
+ node['node_id'] = node_id
138
+ if node.get('nodes'):
139
+ self._add_node_ids(node['nodes'], prefix=f"{node_id}.")
140
+
141
+ def reasoning_search(self, query: str, llm_client: Any, model: str = "gpt-4-turbo") -> str:
142
+ """
143
+ Performs a tree search to find relevant nodes for the query.
144
+
145
+ Args:
146
+ query (str): The user query.
147
+ llm_client: The initialized OpenAI client.
148
+
149
+ Returns:
150
+ str: Context concatenated from relevant nodes.
151
+ """
152
+ if not self.tree:
153
+ return "Tree not index built. Please upload a document first."
154
+
155
+ # Simplify tree for prompt (remove raw text to save tokens, keep titles and IDs)
156
+ tree_summary = self._get_tree_structure_summary(self.tree)
157
+
158
+ prompt = f"""
159
+ You are given a query and the tree structure of a document.
160
+ You need to find all nodes that are likely to contain the answer.
161
+
162
+ Query: {query}
163
+
164
+ Document tree structure:
165
+ {json.dumps(tree_summary, indent=2)}
166
+
167
+ Reply in the following JSON format:
168
+ {{
169
+ "thinking": <your reasoning about which nodes are relevant>,
170
+ "node_list": ["node_id1", "node_id2", ...]
171
+ }}
172
+ """
173
+
174
+ try:
175
+ response = llm_client.chat.completions.create(
176
+ model=model, # Use the provided model instead of hardcoded default
177
+ messages=[{"role": "user", "content": prompt}],
178
+ temperature=0.1
179
+ )
180
+
181
+ content = response.choices[0].message.content
182
+ # Basic JSON extraction if LLM adds markdown
183
+ if "```json" in content:
184
+ content = content.split("```json")[1].split("```")[0].strip()
185
+ elif "```" in content:
186
+ content = content.split("```")[1].split("```")[0].strip()
187
+
188
+ result = json.loads(content)
189
+ relevant_node_ids = result.get("node_list", [])
190
+
191
+ # Retrieve text for these nodes
192
+ context_parts = []
193
+ for node_id in relevant_node_ids:
194
+ node = self._find_node_by_id(self.tree, node_id)
195
+ if node:
196
+ context_parts.append(f"--- Section: {node['title']} (ID: {node_id}) ---\n{node['text']}\n")
197
+
198
+ full_context = "\n".join(context_parts)
199
+ if not full_context:
200
+ return "No relevant context found in the document tree."
201
+ return full_context
202
+
203
+ except Exception as e:
204
+ return f"Error during reasoning search: {str(e)}"
205
+
206
+ def _get_tree_structure_summary(self, nodes):
207
+ summary = []
208
+ for node in nodes:
209
+ node_summary = {
210
+ 'title': node['title'],
211
+ 'node_id': node['node_id']
212
+ }
213
+ if node.get('nodes'):
214
+ node_summary['nodes'] = self._get_tree_structure_summary(node['nodes'])
215
+ summary.append(node_summary)
216
+ return summary
217
+
218
+ def _find_node_by_id(self, nodes, target_id):
219
+ for node in nodes:
220
+ if node.get('node_id') == target_id:
221
+ return node
222
+ if node.get('nodes'):
223
+ found = self._find_node_by_id(node['nodes'], target_id)
224
+ if found:
225
+ return found
226
+ return None