quanliang commited on
Commit
ceaf3bb
·
0 Parent(s):

Initial commit - chatbot online version

Browse files
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
conversation_storage.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from datetime import datetime
3
+ from typing import Dict, List, Any
4
+
5
+ # Global conversation storage dictionary
6
+ SYSTEM_CHART_CONVERSATIONS: Dict[str, Dict[str, Any]] = {}
7
+
8
+ def save_conversation(extraction_id: str, paper_title: str, conversation_history: List, paper_content: str, paper_type: str) -> str:
9
+ """
10
+ Save a conversation to the global conversation storage.
11
+
12
+ Args:
13
+ extraction_id: Unique identifier for the extraction task
14
+ paper_title: Title of the paper
15
+ conversation_history: List of conversation messages
16
+ paper_content: Full content of the paper
17
+ paper_type: Type of paper (Experimental/Practical, Theoretical/Computational, Mixed)
18
+
19
+ Returns:
20
+ str: Unique conversation key
21
+ """
22
+ # Initialize extraction_id entry if it doesn't exist
23
+ if extraction_id not in SYSTEM_CHART_CONVERSATIONS:
24
+ SYSTEM_CHART_CONVERSATIONS[extraction_id] = {}
25
+
26
+ # Create a safe key from paper title (matching original behavior)
27
+ safe_title = re.sub(r'[^\w\s-]', '', paper_title)[:100]
28
+ safe_title = re.sub(r'[-\s]+', '_', safe_title)
29
+
30
+ conversation_key = f"{safe_title}_{len(SYSTEM_CHART_CONVERSATIONS[extraction_id])}"
31
+
32
+ # Save the conversation
33
+ SYSTEM_CHART_CONVERSATIONS[extraction_id][conversation_key] = {
34
+ "paper_title": paper_title,
35
+ "conversation_history": conversation_history.copy(), # Make a copy to avoid modification
36
+ "paper_content": paper_content,
37
+ "paper_type": paper_type,
38
+ "created_at": datetime.now().isoformat(),
39
+ "last_updated": datetime.now().isoformat()
40
+ }
41
+
42
+ print(f"💾 Saved conversation for paper: {paper_title} (key: {conversation_key})")
43
+ return conversation_key
44
+
45
+ def get_conversations(extraction_id: str) -> Dict[str, Dict[str, Any]]:
46
+ """
47
+ Get all conversations for a specific extraction ID.
48
+
49
+ Args:
50
+ extraction_id: Unique identifier for the extraction task
51
+
52
+ Returns:
53
+ Dict of conversation_key -> conversation_data
54
+ """
55
+ return SYSTEM_CHART_CONVERSATIONS.get(extraction_id, {})
56
+
57
+ def update_conversation(extraction_id: str, conversation_key: str, new_message, response):
58
+ """
59
+ Update an existing conversation with a new message and response.
60
+
61
+ Args:
62
+ extraction_id: Unique identifier for the extraction task
63
+ conversation_key: Unique conversation key
64
+ new_message: New message to add
65
+ response: Response to the message
66
+
67
+ Returns:
68
+ bool: True if update was successful, False otherwise
69
+ """
70
+ if (extraction_id in SYSTEM_CHART_CONVERSATIONS and
71
+ conversation_key in SYSTEM_CHART_CONVERSATIONS[extraction_id]):
72
+
73
+ conversation = SYSTEM_CHART_CONVERSATIONS[extraction_id][conversation_key]
74
+ conversation["conversation_history"].append(new_message)
75
+ conversation["conversation_history"].append(response)
76
+ conversation["last_updated"] = datetime.now().isoformat()
77
+ return True
78
+ return False
hypotheses_generation.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import regex as re
2
+ from typing import Dict, List, Tuple
3
+ from langchain_core.messages import HumanMessage
4
+ from prompts import create_hypotheses_generation_prompt
5
+
6
+ from typing import Tuple
7
+
8
+ PATTERN = re.compile(
9
+ r"PAPER:\s*(?P<paper>.*?)\s*[\r\n]+SYSTEM_CHART:\s*(?P<chart>.*?)(?=\s*={5,}|$)",
10
+ re.DOTALL
11
+ )
12
+
13
+ def parse_multiple_system_chart_files(content_list: List[str]) -> Tuple[str, str]:
14
+ """
15
+ Parse multiple system chart files and extract numbered tables and citations.
16
+ Handles new format with 100 '=' delimiters and PAPER:/SYSTEM_CHART: structure.
17
+
18
+ Args:
19
+ content_list: List of raw content strings from multiple system chart files
20
+
21
+ Returns:
22
+ Tuple of (numbered_tables, numbered_citations)
23
+ """
24
+ papers: List[str] = []
25
+ charts: List[str] = []
26
+
27
+ for blob in content_list:
28
+ if not blob or not blob.strip():
29
+ continue
30
+ for m in PATTERN.finditer(blob):
31
+ papers.append(m.group("paper").strip())
32
+ charts.append(m.group("chart").strip())
33
+
34
+ numbered_tables = []
35
+ numbered_citations = []
36
+ for i, (p, c) in enumerate(zip(papers, charts), 1):
37
+ numbered_tables.append(f"\n=== TABLE {i} ===\n{c}\n=== END OF TABLE {i} ===")
38
+ numbered_citations.append(f"{i}. {p}")
39
+
40
+ return "\n".join(numbered_tables).strip(), "\n".join(numbered_citations).strip()
41
+
42
+ def parse_system_charts_and_citations(content: str) -> Tuple[str, str]:
43
+ """
44
+ Parse system charts content and extract numbered tables and citations robustly.
45
+ Updated to handle new format with 100 '=' delimiters and PAPER:/SYSTEM_CHART: structure.
46
+
47
+ Args:
48
+ content: Raw content from system_charts.txt file
49
+
50
+ Returns:
51
+ Tuple of (numbered_tables, numbered_citations)
52
+ """
53
+ # Use the new multi-file parser with single content
54
+ return parse_multiple_system_chart_files([content])
55
+
56
+ async def generate_hypotheses_from_system_charts(
57
+ system_charts: List[Dict[str, str]],
58
+ llm,
59
+ material_goal_1: str = "Property #1 for xxx material",
60
+ material_goal_2: str = "Property #2 for xxx material",
61
+ iterations_count: int = 1,
62
+ progress_callback=None
63
+ ) -> str:
64
+ """
65
+ Generate scientific hypotheses using the new synergistic approach.
66
+
67
+ Args:
68
+ system_charts: List of parsed system chart dictionaries with 'content' or 'raw_content' fields
69
+ llm: Language model for hypothesis generation
70
+ material_goal_1: First material design goal to guide hypothesis generation
71
+ material_goal_2: Second material design goal to guide hypothesis generation
72
+ iterations_count: Number of hypothesis iterations to generate
73
+ progress_callback: Function to report progress updates
74
+
75
+ Returns:
76
+ hypotheses_content
77
+ """
78
+
79
+ if not system_charts:
80
+ return "No system charts provided for hypothesis generation."
81
+
82
+ # Initial progress update
83
+ if progress_callback:
84
+ progress_callback({
85
+ "status": "parsing",
86
+ "progress": 5,
87
+ "message": "Parsing system charts and citations..."
88
+ })
89
+
90
+ # Extract raw content from system charts
91
+ # Support both 'raw_content' and 'content' fields for backward compatibility
92
+ content_list = []
93
+ for chart in system_charts:
94
+ raw_content = chart.get('raw_content', chart.get('content', ''))
95
+ if raw_content and raw_content.strip():
96
+ content_list.append(raw_content)
97
+
98
+ if not content_list:
99
+ return "No valid system chart content found for hypothesis generation."
100
+
101
+ # Parse all system chart files together using the new multi-file parser
102
+ numbered_tables, numbered_citations = parse_multiple_system_chart_files(content_list)
103
+
104
+ if progress_callback:
105
+ progress_callback({
106
+ "status": "generating",
107
+ "progress": 20,
108
+ "message": "Generating synergistic hypotheses..."
109
+ })
110
+
111
+ # Create the prompt with user-specified material goals
112
+ hypotheses_prompt = create_hypotheses_generation_prompt(material_goal_1, material_goal_2)
113
+
114
+ # Prepare the prompt in the specified format
115
+ full_prompt = f"Tables are: {numbered_tables}\n\nCitation resources are: {numbered_citations}\n\n{hypotheses_prompt}"
116
+
117
+ # Configure LLM parameters for creativity
118
+ llm_kwargs = {}
119
+
120
+ # Check if this is a reasoning model (o1 series) which doesn't support temperature
121
+ model_name = getattr(llm, 'model_name', getattr(llm, 'model', ''))
122
+ if not (model_name and model_name.lower().startswith('o')):
123
+ # Set higher temperature for creativity (non-reasoning models)
124
+ if hasattr(llm, 'temperature'):
125
+ original_temp = llm.temperature
126
+ llm.temperature = 1.0 # Higher temperature for creativity
127
+ elif hasattr(llm, 'model_kwargs'):
128
+ llm_kwargs = llm.model_kwargs.copy()
129
+ llm_kwargs['temperature'] = 1.0
130
+
131
+ try:
132
+ # Generate hypotheses for each iteration
133
+ all_iterations_content = []
134
+
135
+ for iteration in range(iterations_count):
136
+ # Update progress for current iteration
137
+ if progress_callback:
138
+ progress_start = 20 + (iteration * 60 // iterations_count)
139
+ progress_callback({
140
+ "status": "llm_processing",
141
+ "progress": progress_start,
142
+ "message": f"LLM generating hypotheses (iteration {iteration + 1}/{iterations_count})..."
143
+ })
144
+
145
+ conversation_history = [HumanMessage(content=full_prompt)]
146
+
147
+ # Generate with appropriate parameters
148
+ if llm_kwargs:
149
+ # Use model_kwargs if available
150
+ original_kwargs = getattr(llm, 'model_kwargs', {})
151
+ llm.model_kwargs = llm_kwargs
152
+ response = await llm.ainvoke(conversation_history)
153
+ llm.model_kwargs = original_kwargs
154
+ else:
155
+ response = await llm.ainvoke(conversation_history)
156
+
157
+ iteration_content = response.content
158
+ all_iterations_content.append({
159
+ 'iteration': iteration + 1,
160
+ 'content': iteration_content
161
+ })
162
+
163
+ if progress_callback:
164
+ progress_callback({
165
+ "status": "formatting",
166
+ "progress": 80,
167
+ "message": "Formatting results..."
168
+ })
169
+
170
+ # Create formatted output with all iterations
171
+ iterations_output = ""
172
+ for iteration_data in all_iterations_content:
173
+ iterations_output += f"\n\n"
174
+ iterations_output += "=" * 80 + "\n"
175
+ iterations_output += f"ITERATION {iteration_data['iteration']} HYPOTHESES:\n"
176
+ iterations_output += "=" * 80 + "\n\n"
177
+ iterations_output += iteration_data['content'] + "\n\n"
178
+ iterations_output += "=" * 80 + "\n"
179
+
180
+ # Create formatted output
181
+ formatted_output = ""
182
+ formatted_output += "NUMBERED SYSTEM CHARTS:\n"
183
+ formatted_output += numbered_tables + "\n\n"
184
+ formatted_output += "CITATION REFERENCES:\n"
185
+ formatted_output += numbered_citations
186
+ formatted_output += iterations_output
187
+
188
+ # Restore original temperature if modified
189
+ if hasattr(llm, 'temperature') and 'original_temp' in locals():
190
+ llm.temperature = original_temp
191
+
192
+ if progress_callback:
193
+ progress_callback({
194
+ "status": "completed",
195
+ "progress": 100,
196
+ "message": f"Generated {iterations_count} iteration{'s' if iterations_count > 1 else ''} of synergistic hypotheses from {len(system_charts)} charts"
197
+ })
198
+
199
+ return formatted_output
200
+
201
+ except Exception as e:
202
+ # Restore original temperature if modified
203
+ if hasattr(llm, 'temperature') and 'original_temp' in locals():
204
+ llm.temperature = original_temp
205
+
206
+ error_msg = f"Error generating hypotheses: {str(e)}"
207
+ return error_msg
hypotheses_service.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import BytesIO
2
+ from zipfile import ZipFile
3
+ from typing import Dict, Callable, Any, List
4
+ from hypotheses_generation import generate_hypotheses_from_system_charts
5
+
6
+ class HypothesesService:
7
+ """
8
+ Service class for Scientific Hypotheses Generation.
9
+ Handles the orchestration of parsing system charts, generating hypotheses,
10
+ and packaging results using the new synergistic approach.
11
+ """
12
+
13
+ def __init__(self):
14
+ pass
15
+
16
+ async def generate_hypotheses(
17
+ self,
18
+ system_charts_content_list: List[str],
19
+ llm,
20
+ material_goal_1: str = "Property #1 for xxx material",
21
+ material_goal_2: str = "Property #2 for xxx material",
22
+ iterations_count: int = 1,
23
+ progress_callback: Callable[[Dict[str, Any]], None] = None
24
+ ) -> Dict[str, Any]:
25
+ """
26
+ Main hypotheses generation pipeline using synergistic approach.
27
+
28
+ Args:
29
+ system_charts_content_list: List of content strings from multiple system chart files
30
+ llm: Language model for hypothesis generation
31
+ material_goal_1: First material design goal to guide hypothesis generation
32
+ material_goal_2: Second material design goal to guide hypothesis generation
33
+ iterations_count: Number of hypothesis iterations to generate
34
+ progress_callback: Function to report progress updates
35
+
36
+ Returns:
37
+ Dict containing generation results and metadata
38
+ """
39
+ try:
40
+ # Initial progress update
41
+ if progress_callback:
42
+ progress_callback({
43
+ "status": "starting",
44
+ "progress": 0,
45
+ "message": "Preparing for synergistic hypothesis generation..."
46
+ })
47
+
48
+ # Handle both single content and list of contents for backward compatibility
49
+ if isinstance(system_charts_content_list, str):
50
+ system_charts_content_list = [system_charts_content_list]
51
+
52
+ # Create chart data structure for multiple files
53
+ chart_data = []
54
+ for i, content in enumerate(system_charts_content_list, 1):
55
+ if content and content.strip():
56
+ chart_data.append({
57
+ 'content': content,
58
+ 'citation': f'System chart file {i}',
59
+ 'title': f'System Chart File {i}',
60
+ 'raw_content': content # Pass the raw content
61
+ })
62
+
63
+ if not chart_data:
64
+ return {
65
+ "success": False,
66
+ "message": "No valid system chart content provided for hypothesis generation.",
67
+ "zip_data": None,
68
+ "filename": None
69
+ }
70
+
71
+ print(f"📊 Prepared {len(chart_data)} system chart files for synergistic hypothesis generation")
72
+
73
+ # Update progress after parsing
74
+ if progress_callback:
75
+ progress_callback({
76
+ "status": "parsing_complete",
77
+ "progress": 10,
78
+ "total_charts": len(chart_data),
79
+ "message": f"System charts prepared from {len(chart_data)} files. Starting synergistic hypothesis generation..."
80
+ })
81
+
82
+ # Create progress callback function for the generation process
83
+ def generation_progress_callback(progress_data):
84
+ if progress_callback:
85
+ # Adjust progress to fit within our overall range (10% to 95%)
86
+ adjusted_progress = 10 + int((progress_data.get("progress", 0) / 100) * 85)
87
+ progress_data["progress"] = adjusted_progress
88
+ progress_callback(progress_data)
89
+
90
+ # Generate hypotheses using the new synergistic approach
91
+ print(f"🧠 Starting synergistic hypothesis generation with goals: '{material_goal_1}' and '{material_goal_2}' ({iterations_count} iteration{'s' if iterations_count > 1 else ''})...")
92
+ hypotheses_content = await generate_hypotheses_from_system_charts(
93
+ chart_data,
94
+ llm,
95
+ material_goal_1,
96
+ material_goal_2,
97
+ iterations_count,
98
+ generation_progress_callback
99
+ )
100
+
101
+ # Note: Cross-chart analysis is now integrated into the main generation process
102
+ cross_analysis_content = "" # No longer separate
103
+
104
+ # Create ZIP file in memory
105
+ if progress_callback:
106
+ progress_callback({
107
+ "status": "packaging",
108
+ "progress": 95,
109
+ "message": "Creating results package..."
110
+ })
111
+
112
+ zip_data = self._create_zip_file(
113
+ hypotheses_content,
114
+ cross_analysis_content
115
+ )
116
+
117
+ # Prepare result
118
+ return {
119
+ "success": True,
120
+ "message": f"Generated {iterations_count} iteration{'s' if iterations_count > 1 else ''} of synergistic hypotheses from {len(chart_data)} system chart files using cross-paper analysis approach!",
121
+ "zip_data": zip_data,
122
+ "filename": "hypotheses.zip",
123
+ "charts_processed": len(chart_data),
124
+ "cross_analysis_included": True, # Now always included in main process
125
+ "method": "synergistic_analysis",
126
+ "iterations_count": iterations_count
127
+ }
128
+
129
+ except Exception as e:
130
+ return {
131
+ "success": False,
132
+ "message": f"Error during synergistic hypothesis generation: {str(e)}",
133
+ "zip_data": None,
134
+ "filename": None
135
+ }
136
+
137
+ def _create_zip_file(
138
+ self,
139
+ hypotheses_content: str,
140
+ cross_analysis_content: str = ""
141
+ ) -> bytes:
142
+ """Create ZIP file containing hypothesis generation results."""
143
+ zip_buffer = BytesIO()
144
+
145
+ with ZipFile(zip_buffer, "w") as zipf:
146
+ # Add main hypotheses file
147
+ zipf.writestr("hypotheses.txt", hypotheses_content)
148
+
149
+ print(f"📦 Created synergistic hypotheses ZIP package with {len(zipf.namelist())} files")
150
+
151
+ # Get ZIP data and close buffer
152
+ zip_data = zip_buffer.getvalue()
153
+ zip_buffer.close()
154
+
155
+ return zip_data
156
+
157
+ @staticmethod
158
+ def validate_multiple_system_charts_content(content_list: List[str]) -> Dict[str, Any]:
159
+ """
160
+ Validate that the provided content list is suitable for synergistic hypothesis generation.
161
+
162
+ Args:
163
+ content_list: List of content strings to validate
164
+
165
+ Returns:
166
+ Dict with validation results
167
+ """
168
+ try:
169
+ if not content_list:
170
+ return {
171
+ "valid": False,
172
+ "message": "No content provided for hypothesis generation.",
173
+ "chart_count": 0
174
+ }
175
+
176
+ valid_files = 0
177
+ total_data_rows = 0
178
+ file_details = []
179
+
180
+ for i, content in enumerate(content_list, 1):
181
+ if not content or not content.strip():
182
+ file_details.append(f"File {i}: Empty content")
183
+ continue
184
+
185
+ # Check for new format with 100 '=' delimiters
186
+ lines = content.strip().split('\n')
187
+ has_new_format = any(line.strip() == '=' * 100 for line in lines)
188
+ has_paper_keywords = any(line.strip().startswith('PAPER:') for line in lines)
189
+ has_chart_keywords = any(line.strip().startswith('SYSTEM_CHART:') for line in lines)
190
+
191
+ if has_new_format and has_paper_keywords and has_chart_keywords:
192
+ # Count entries in new format
193
+ paper_count = len([line for line in lines if line.strip().startswith('PAPER:')])
194
+ chart_count = len([line for line in lines if line.strip().startswith('SYSTEM_CHART:')])
195
+
196
+ if paper_count > 0 and chart_count > 0 and paper_count == chart_count:
197
+ valid_files += 1
198
+ total_data_rows += paper_count
199
+ file_details.append(f"File {i}: Valid format with {paper_count} entries")
200
+ else:
201
+ file_details.append(f"File {i}: Format issues - {paper_count} papers, {chart_count} charts")
202
+ else:
203
+ # Fallback to table-based validation
204
+ table_lines = [line for line in lines if '|' in line and not line.strip().startswith('|---')]
205
+
206
+ if table_lines:
207
+ data_rows = len([line for line in table_lines if line.strip() and (
208
+ ('|' in line) and not any(
209
+ header_word in line.lower() for header_word in ['processing', 'structure', 'property', 'mechanism']
210
+ )
211
+ )])
212
+
213
+ if data_rows >= 2:
214
+ valid_files += 1
215
+ total_data_rows += data_rows
216
+ file_details.append(f"File {i}: Valid table format with {data_rows} rows")
217
+ else:
218
+ file_details.append(f"File {i}: Insufficient table data ({data_rows} rows)")
219
+ else:
220
+ file_details.append(f"File {i}: No valid format found")
221
+
222
+ if valid_files == 0:
223
+ return {
224
+ "valid": False,
225
+ "message": f"No valid system chart files found. Details: {'; '.join(file_details)}",
226
+ "chart_count": 0,
227
+ "valid_charts": 0,
228
+ "file_details": file_details
229
+ }
230
+
231
+ if total_data_rows < 2:
232
+ return {
233
+ "valid": False,
234
+ "message": f"Insufficient data across all files. Need multiple entries for synergistic analysis. Found {total_data_rows} total entries.",
235
+ "chart_count": len(content_list),
236
+ "valid_charts": valid_files,
237
+ "file_details": file_details
238
+ }
239
+
240
+ return {
241
+ "valid": True,
242
+ "message": f"Validated {valid_files}/{len(content_list)} system chart files for synergistic hypothesis generation. Found {total_data_rows} total entries.",
243
+ "chart_count": len(content_list),
244
+ "valid_charts": valid_files,
245
+ "data_entries": total_data_rows,
246
+ "file_details": file_details
247
+ }
248
+
249
+ except Exception as e:
250
+ return {
251
+ "valid": False,
252
+ "message": f"Error validating content: {str(e)}",
253
+ "chart_count": 0,
254
+ "file_details": []
255
+ }
256
+
257
+ @staticmethod
258
+ def validate_system_charts_content(content) -> Dict[str, Any]:
259
+ """
260
+ Validate that the provided content is suitable for synergistic hypothesis generation.
261
+ Handles both single content string and list of content strings.
262
+
263
+ Args:
264
+ content: Content to validate (str or List[str])
265
+
266
+ Returns:
267
+ Dict with validation results
268
+ """
269
+ # Handle both single content and list of contents
270
+ if isinstance(content, str):
271
+ return HypothesesService.validate_multiple_system_charts_content([content])
272
+ elif isinstance(content, list):
273
+ return HypothesesService.validate_multiple_system_charts_content(content)
274
+ else:
275
+ return {
276
+ "valid": False,
277
+ "message": "Invalid content type. Expected string or list of strings.",
278
+ "chart_count": 0
279
+ }
index.html ADDED
The diff for this file is too large to render. See raw diff
 
main.py ADDED
The diff for this file is too large to render. See raw diff
 
pdf_text_processing.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import PyPDF2
3
+ import re
4
+ from langchain_core.documents import Document
5
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
6
+
7
+
8
+ def detect_file_type(file_path: str) -> str:
9
+ """
10
+ Detect file type based on extension.
11
+ Returns 'pdf' or 'text'.
12
+ """
13
+ _, ext = os.path.splitext(file_path.lower())
14
+ if ext == '.pdf':
15
+ return 'pdf'
16
+ elif ext in ['.txt', '.text']:
17
+ return 'text'
18
+ else:
19
+ # Default to text for unknown extensions
20
+ return 'text'
21
+
22
+
23
+ def get_text_title(file_path: str) -> str:
24
+ """
25
+ Extract title from text file that follows the format "Title: XXXXX Abstract: XXXXX"
26
+ """
27
+ try:
28
+ with open(file_path, "r", encoding="utf-8") as f:
29
+ content = f.read()
30
+
31
+ # Look for "Title:" pattern (case insensitive)
32
+ title_match = re.search(r'title:\s*([^\n\r]+?)(?:\s+abstract:|$)', content, re.IGNORECASE)
33
+ if title_match:
34
+ title = title_match.group(1).strip()
35
+ # Clean up the title
36
+ title = re.sub(r'[^\w\s-]', '', title)[:100]
37
+ if title:
38
+ return title
39
+
40
+ # If no "Title:" found, try to get the first non-empty line
41
+ lines = content.strip().split('\n')
42
+ for line in lines:
43
+ line = line.strip()
44
+ if line and not line.lower().startswith('abstract:'):
45
+ # Remove common trailing punctuation from titles
46
+ title = line.rstrip('.,;:!?')
47
+ return title[:100]
48
+
49
+ except Exception as e:
50
+ print(f"❌ Error reading text file {file_path}: {e}")
51
+
52
+ return os.path.basename(file_path)
53
+
54
+
55
+ def get_pdf_title(file_path: str) -> str:
56
+ """
57
+ Existing code that attempts to get a PDF title.
58
+ """
59
+ try:
60
+ with open(file_path, "rb") as f:
61
+ reader = PyPDF2.PdfReader(f)
62
+ meta = reader.metadata
63
+ if meta and meta.title:
64
+ title = re.sub(r'[^\w\s-]', '', meta.title.strip())[:100]
65
+ return title
66
+ first_page = reader.pages[0].extract_text()
67
+ if first_page:
68
+ return first_page.strip().split('\n')[0]
69
+ except:
70
+ pass
71
+ return os.path.basename(file_path)
72
+
73
+
74
+ def get_file_title(file_path: str) -> str:
75
+ """
76
+ Get title from either PDF or text file based on file type.
77
+ """
78
+ file_type = detect_file_type(file_path)
79
+ if file_type == 'pdf':
80
+ return get_pdf_title(file_path)
81
+ else:
82
+ return get_text_title(file_path)
83
+
84
+
85
+ def remove_duplicate_chunks(docs):
86
+ """
87
+ Your existing deduplication logic.
88
+ """
89
+ seen = set()
90
+ unique_docs = []
91
+ for doc in docs:
92
+ key = (doc.page_content.strip(), doc.metadata.get("title", ""))
93
+ if key not in seen:
94
+ seen.add(key)
95
+ unique_docs.append(doc)
96
+ return unique_docs
97
+
98
+ def extract_text_from_file(file_path: str) -> list:
99
+ """
100
+ Extract text from either PDF or text file.
101
+ Returns list of Document objects (typically length=1).
102
+ """
103
+ docs = []
104
+ try:
105
+ file_type = detect_file_type(file_path)
106
+ title = get_file_title(file_path)
107
+
108
+ if file_type == 'pdf':
109
+ # Handle PDF files
110
+ with open(file_path, "rb") as f:
111
+ reader = PyPDF2.PdfReader(f)
112
+ text = "\n".join([p.extract_text() or "" for p in reader.pages])
113
+ else:
114
+ # Handle text files
115
+ with open(file_path, "r", encoding="utf-8") as f:
116
+ text = f.read()
117
+
118
+ if text.strip():
119
+ docs = [Document(page_content=text, metadata={"source": os.path.basename(file_path), "title": title, "file_type": file_type})]
120
+
121
+ except Exception as e:
122
+ print(f"❌ Error processing {file_path}: {e}")
123
+
124
+ return docs
125
+
126
+
127
+ def extract_text_from_pdf(file_path: str) -> list:
128
+ """
129
+ Legacy function name for backward compatibility.
130
+ Now handles both PDF and text files.
131
+ """
132
+ return extract_text_from_file(file_path)
133
+
134
+
135
+ def extract_and_split(file_path: str):
136
+ """
137
+ Extract text from file (PDF or text) and split into chunks.
138
+ """
139
+ docs = []
140
+ try:
141
+ file_type = detect_file_type(file_path)
142
+ title = get_file_title(file_path)
143
+
144
+ if file_type == 'pdf':
145
+ # Handle PDF files
146
+ with open(file_path, "rb") as f:
147
+ reader = PyPDF2.PdfReader(f)
148
+ text = "\n".join([p.extract_text() or "" for p in reader.pages])
149
+ else:
150
+ # Handle text files
151
+ with open(file_path, "r", encoding="utf-8") as f:
152
+ text = f.read()
153
+
154
+ if not text.strip():
155
+ return []
156
+
157
+ splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
158
+ docs = splitter.create_documents([text], metadatas=[{"source": os.path.basename(file_path), "title": title, "file_type": file_type}])
159
+ docs = remove_duplicate_chunks(docs)
160
+ print(f"📄 {title}: {len(docs)} unique chunks after deduplication")
161
+
162
+ except Exception as e:
163
+ print(f"❌ Error processing {file_path}: {e}")
164
+
165
+ return docs
166
+
167
+ def entity_match(dictionary: dict, target_phrase: str, window_size: int = 70) -> dict:
168
+ """
169
+ Given one entity (target_phrase), return a dict:
170
+ {paper_name: [list_of_snippets]}
171
+ Each snippet is a window of ±`window_size` words around the matched term.
172
+ """
173
+ output = {}
174
+ phrase_tokens = target_phrase.lower().split()
175
+ phrase_len = len(phrase_tokens)
176
+
177
+ for paper_name, text in dictionary.items():
178
+ words = text.split()
179
+ n = len(words)
180
+
181
+ intervals = []
182
+
183
+ i = 0
184
+ while i < n:
185
+ # Enough room for entire phrase
186
+ if i + phrase_len <= n:
187
+ slice_candidate = words[i : i + phrase_len]
188
+ if [w.lower() for w in slice_candidate] == phrase_tokens:
189
+ start_idx = max(0, i - window_size)
190
+ end_idx = min(n, i + phrase_len + window_size)
191
+ intervals.append((start_idx, end_idx))
192
+ # Advance i by 1 to allow overlapping matches
193
+ i += 1
194
+ continue
195
+ i += 1
196
+
197
+ # If no intervals found, just store empty list and move on
198
+ if not intervals:
199
+ output[paper_name] = []
200
+ continue
201
+
202
+ # Sort intervals by their start index
203
+ intervals.sort(key=lambda x: x[0])
204
+
205
+ # Merge overlapping intervals
206
+ merged_intervals = []
207
+ current_start, current_end = intervals[0]
208
+
209
+ for idx in range(1, len(intervals)):
210
+ start, end = intervals[idx]
211
+ if start <= current_end: # overlap
212
+ current_end = max(current_end, end)
213
+ else:
214
+ merged_intervals.append((current_start, current_end))
215
+ current_start, current_end = start, end
216
+
217
+ # Add the final interval
218
+ merged_intervals.append((current_start, current_end))
219
+
220
+ # Convert merged intervals to text snippets
221
+ snippets = []
222
+ for (start_i, end_i) in merged_intervals:
223
+ snippet_words = words[start_i:end_i]
224
+ snippet_text = " ".join(snippet_words)
225
+ snippets.append(snippet_text)
226
+
227
+ output[paper_name] = snippets
228
+
229
+ return output
prompts.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- For system chart extraction ----
2
+
3
+ PROMPTS_CLASSIFICATION = '''In the field of materials science research, scholarly papers typically align with one of three research paradigms:
4
+
5
+ Experimental/Practical Research: These papers are grounded in experimental methodologies, detailing the use of specific material processing techniques to achieve desired material structures. The focus is on empirically demonstrating how these processing methods influence the material's mechanical, thermal, chemical, electrical, or other relevant properties. While the paper may include brief computational analysis methods, these are not the main subject or the primary emphasis. Papers in this category typically feature experimental data, analyses of structural characteristics, and discussions on how processing conditions affect the material's performance.
6
+
7
+ Theoretical/Computational Research: These papers focus solely on computational models or simulations, employing methods like density functional theory (DFT), molecular dynamics (MD), finite element analysis, thermodynamic calculations, or other modeling approaches to predict and analyze the behavior and performance of materials at various scales (atomic, molecular, or macroscopic level). The focus is on theoretical predictions and simulations, excluding any actual processing of materials. Such papers discuss the theoretical framework, present simulation results, and explore the implications of these findings for potential material processing strategies.
8
+
9
+ Mixed Research: These papers combine experimental and computational approaches, employing computational techniques to conceptualize potential materials or processes, followed by experimental procedures to synthesize these materials and evaluate their characteristics. The content includes substantial theoretical analysis and extensive experimental research, with both parts occupying a significant portion of the text.
10
+
11
+ Please evaluate the attached paper and determine which category it best fits. Provide a succinct classification and reasoning for your decision, considering aspects like the research methodology, primary focus (experimental vs. theoretical), data analysis, and the nature of the conclusions drawn.
12
+
13
+ Strictly respond in the following format:
14
+ Category: [Experimental/Practical, Theoretical/Computational, or Mixed]
15
+ Reasons: [Offer a concise explanation, referencing specific elements such as the research methods (experimental or computational), the primary focus of the paper, the type of data presented (experimental results or simulation data), and how the conclusions relate to practical applications or theoretical advancements in the field of materials science.]'''
16
+
17
+
18
+ PROMPTS_EXPERIMENTAL = ['''
19
+ Identify the material's significant properties discussed in the text. Include only those properties explicitly mentioned in the text, avoiding any not directly addressed. Prioritize information provided in the Abstract and Conclusion sections. Ensure each property description is both concise and precise. Please include the environmental condition for each property, provided that the property is valid under that specific environmental condition. Use semicolons to separate your answers. Please make sure your answer is no more than 15 words in total.''', # Get the target property.
20
+
21
+ '''
22
+ Extract key structural characteristics relevant to the material's properties or behavior mechanisms as concise, noun-based phrases; avoid full sentences. Separate responses with semicolons. Please make sure your answer is no more than 20 words in total.''', # Get the structure of the material.
23
+
24
+ '''
25
+ Perorm a detailed analysis to identify the composition and primary functional components of this material. Ensure they are distinguished from the structure discussed previously; in other words, they should not overlap with answers from earlier structure questions. Provide only the results, without any additional information or explanation. Respond in this format: Composition: YOUR ANSWER; Key Components: YOUR ANSWER. If no such component is identified, state 'N/A'.''', # Get the composition and key components (if any) of the material.
26
+
27
+ '''
28
+ 1. Construct a table linking structure, associated mechanisms, and resultant material properties. Consider only the structures and properties outlined in the prerequisites, without altering them when completing the table. The mechanisms described in the paper should always take precedence. Present your findings in a table format with three columns: Structure, Mechanism (Structure & Property), and Property. Include every listed structure and describe the mechanism in detail to prevent ambiguity.
29
+ 2. Please include the effects of environmental conditions on structural characteristics within the mechanisms, if the corresponding property is influenced by environmental conditions.
30
+ 3. To ensure clarity and accuracy in indicating the source of each mechanism (except when N/A), adhere to the following guidelines in your responses: For Mechanisms Directly from the Original Text: If the explanation or mechanism you provide is directly taken or closely paraphrased from the original text, indicate this using the format: "XXX (From text)". For Mechanisms Derived from Your Own Knowledge Base: If the explanation or mechanism is not found in the original text and instead comes from your understanding or external knowledge, use the format: "XXX (From knowledge base)". Please strictly follow this format for each mechanism you explain.
31
+ ''', # Get the mechanism between the target property and the structure.
32
+
33
+ '''List the key processing or manufacturing techniques explicitly stated in the text provided for this material; do not base your answers on typical experience. Avoid mentioning details about micro/macro structures. Use semicolons to separate your answers. Additional Requirements: 1. Exclude any processing methods that are hypothetical or proposed as future improvements. Only include those explicitly employed in the current context of the material's manufacturing or processing. 2. Focus solely on manufacturing/synthesis methods, excluding analytical methods like testing and characterization techniques.
34
+ ''', # Get the processing method of the material.
35
+
36
+ '''From the list of processing methods of this material discussed previously, identify the method(s) most central to the paper's discussion. For instance, if process A is highlighted frequently, consider it the primary method. Exclude routine precursor preparation steps unless the paper emphasizes them as critical. Additionally, provide a brief explanation as to why this method, for example, process A, is emphasized in the paper over others, based on the frequency of its mention and its relevance to the key findings or conclusions. This will help to understand the context and significance of the chosen method(s) in the overall study. Ensure to exclude any processing methods not explicitly mentioned in the text.
37
+ RETURN ME STRICTLY WITH THIS FORMAT: The final processing methods are: YOUR ANSWER; The explanations are: YOUR EXPLANATIONS.''', # Refine the processing method of the material.
38
+
39
+ '''
40
+ 1. Construct a table that maps structures to their influential processing methods and the mechanisms responsible for their formation. Frame the relationship as follows: "The [processing method] yields [structure] via the [mechanism]." Ensure a logical and coherent connection among these three elements. The table should consist of three columns titled 'Processing', 'Mechanism (Processing & Structure)', and 'Structure'. Incorporate all relevant structures, omitting those not pertinent to the discussion. Consider only the processing methods specified in the 'processing list'. Each structure must be matched with pertinent processing methods and their mechanisms, emphasizing a clear and logical link among them. The table's content should be concise, focusing exclusively on the interplay between processing methods, mechanisms, and structures.
41
+ 2. For clarity and precision in citing the origin of each mechanism (except when not applicable), adhere to the guidelines below in your responses: For mechanisms directly quoted or closely paraphrased from the original text, use the format: "XXX (From text)". For mechanisms derived from your own knowledge or external sources, use the format: "XXX (From knowledge base)". Strictly follow this format for every mechanism explanation provided.''', # Get the mechanism between the structure and the processing.
42
+
43
+ "Review the provided list that details the composition of the material, including any special component additions. Analyze and identify any components in the list that significantly influence the material's structure. If any such components are identified, add them to the 'Processing' column of the final table's corresponding row. Please do not overwrite the content in the 'Processing' column of the original table unless it is marked as N/A, and refrain from creating a new column for composition/key components.", # Try to add composition and key components into the Processing column.
44
+
45
+ '''Merge the two extracted tables into a single comprehensive table, with the 'Structure' column serving as the primary criterion for merging. Here are the specific instructions:
46
+ 1. Merging Based on Structure: Combine the tables into one, aligning entries based on similarities or matches in the 'Structure' column.
47
+ 2. Handling Non-Matching Structures: For entries in the 'Structure' column that do not match, analyze their relevance and importance based on the context provided by the source papers.
48
+ 3. Decision on Exclusion: After analysis, decide whether the non-matching structure entries are essential to the merged table's overall content and purpose. If not, consider removing these entries to maintain coherence and relevance.
49
+ 4. Columns of the final table: Include Processing, Mechanism (Processing & Structure), Structure, Mechanism (Structure & Property), and Property.
50
+ 5. Ensure the final table is complete, with no gaps. Adjust content as needed for accuracy and consistency with the original texts.
51
+ 6. Mark all uncertain positions as 'N/A'. Replace all blank positions with 'N/A'.
52
+ 7. Prune the table: Examine each row; remove rows with three or more 'N/A' entries. Keep rows with fewer than three 'N/A' entries. If significant information is lost due to pruning, incorporate essential details into other rows, ensuring data coherence and rationale are preserved.
53
+ 8. After completing the above steps, review the document to assess the content's rationality and identify any extraneous content for removal.
54
+ 9. Only the final, corrected table is required; details about the process are not needed.
55
+ The goal is to produce a unified table that accurately reflects the combined data, focusing on maintaining relevance and coherence, especially regarding the 'Structure' column.''' # Merge the two tables.
56
+ ]
57
+
58
+
59
+ PROMPTS_COMPUTATIONAL = [
60
+ '''Identify the material's significant properties discussed in the text. Include only those properties explicitly mentioned in the text, avoiding any not directly addressed. Prioritize information provided in the Abstract and Conclusion sections. Ensure each property description is both concise and precise. Please include the environmental condition for each property, provided that the property is valid under that specific environmental condition. Use semicolons to separate your answers. Please make sure your answer is no more than 15 words in total.''', # Get the target property.
61
+
62
+ "Extract key structural characteristics relevant to the material's properties or behavior mechanisms as concise, noun-based phrases; avoid full sentences. Separate responses with semicolons. Please make sure your answer is no more than 20 words in total.", # Get the structure of the material.
63
+
64
+ "Perorm a detailed analysis to identify the composition and primary functional components of this material. Ensure they are distinguished from the structure discussed previously; in other words, they should not overlap with answers from earlier structure questions. Provide only the results, without any additional information or explanation. Respond in this format: Composition: YOUR ANSWER; Key Components: YOUR ANSWER. If no such component is identified, state 'N/A'.", # Get the composition and key components (if any) of the material.
65
+
66
+ '''
67
+ 1. Construct a table linking structure, associated mechanisms, and resultant material properties. Consider only the structures and properties outlined in the prerequisites, without altering them when completing the table. The mechanisms described in the paper should always take precedence. Present your findings in a table format with three columns: Structure, Mechanism (Structure & Property), and Property. Include every listed structure and describe the mechanism in detail to prevent ambiguity.
68
+ 2. Please include the effects of environmental conditions on structural characteristics within the mechanisms, if the corresponding property is influenced by environmental conditions.
69
+ 3. To ensure clarity and accuracy in indicating the source of each mechanism (except when N/A), adhere to the following guidelines in your responses: For Mechanisms Directly from the Original Text: If the explanation or mechanism you provide is directly taken or closely paraphrased from the original text, indicate this using the format: "XXX (From text)". For Mechanisms Derived from Your Own Knowledge Base: If the explanation or mechanism is not found in the original text and instead comes from your understanding or external knowledge, use the format: "XXX (From knowledge base)". Please strictly follow this format for each mechanism you explain.
70
+ ''', # Get the mechanism between the target property and the structure.
71
+
72
+ '''
73
+ 1. Construct a table that maps structures to their critical compositions/key components and the mechanisms responsible for their formation. This table should consist of three columns: 'Composition/key components', 'Mechanism (Composition/key components & Structure)', and 'Structure'. Incorporate all relevant structures provided and omit those that do not apply to the context. For each structure, if a direct causation from composition or key components is evident, list these in the 'Composition/key components' column; otherwise, enter 'N/A'. Ensure a clear logical relationship among these three categories. The table's details should be concise, focusing specifically on the interplay between the composition/key components, mechanisms, and structures.
74
+ 2. To maintain clarity and precision when referencing the origins of each mechanism (except when N/A), adhere to the following instructions in your responses: For mechanisms directly extracted or closely paraphrased from the original text, use the notation: "XXX (From text)". For mechanisms that originate from your personal knowledge or external sources, apply the notation: "XXX (From knowledge base)". Please strictly follow this notation for each mechanism described.
75
+ ''', # Get the mechanism between the structure and the composition/key components.
76
+
77
+ '''Merge the two extracted tables into a single comprehensive table, focusing on the 'Structure' column as the primary criterion for merging. Follow these specific instructions:
78
+
79
+ 1. Merging Based on Structure: Combine the two tables into one, aligning entries by matching or similar 'Structure' column values.
80
+ 2. Handling Non-Matching Structures: For 'Structure' column entries that do not match between the two tables, analyze their relevance and importance based on the context of the source papers.
81
+ 3. Decision on Exclusion: Decide whether to include non-matching structure entries in the merged table based on their significance to its overall content and purpose. Exclude entries that are not crucial to maintain coherence and relevance.
82
+ 4. Columns of the Final Table: Include the following columns - Composition/Key Components, Mechanism (Composition/Key Components & Structure), Structure, Mechanism (Structure & Property), and Property.
83
+ 5. Ensure the table is complete, with no missing information. Modify content as necessary for accuracy and consistency with the original sources.
84
+ 6. Represent all uncertain data as 'N/A'. Replace any blank fields with 'N/A'.
85
+ 7. Perform Pruning: Review each row for instances of 'N/A'. Remove rows with three or more 'N/A' entries, but strive to incorporate essential details from such rows into others, preserving data coherence and rationale.
86
+ 8. After completing these steps, reassess the document to identify and eliminate any irrelevant content, ensuring the table's rationality and relevance.
87
+ 9. Only the final, corrected table is required without the need for detailed explanations.
88
+ The goal is to produce a unified table that accurately represents the merged data, with an emphasis on relevance and coherence, particularly in relation to the 'Structure' column.
89
+
90
+ Final modifications: If 'Processing' still shows in the table, replace it to 'Composition/key components'. The content within the column should remain unchanged. If 'Mechanism (Processing & Structure)' still shows in the table, replace it to 'Mechanism (Composition/key components & Structure)'. The content within the column should remain unchanged.''' # Final modifications and merge the two tables.
91
+ ]
92
+
93
+ PROMPTS_MIXED = ['''As discussed earlier, this paper is comprehensive, combining both experimental techniques and computational models. Please divide the paper into three sections: A) Computational model and theoretical analysis of the material, B) Material synthesis and manufacturing, and C) Material's performance testing. Keep in mind not to submit the results of your division.
94
+ Identify the material's significant properties discussed in the text. Include only those properties explicitly mentioned in the text, avoiding any not directly addressed. Prioritize information provided in the Abstract and Conclusion sections. Ensure each property description is both concise and precise. Please include the environmental condition for each property, provided that the property is valid under that specific environmental condition. Use semicolons to separate your answers. Please make sure your answer is no more than 15 words in total.''', # Get the target property.
95
+
96
+ "Extract key structural characteristics relevant to the material's properties or behavior mechanisms as concise, noun-based phrases; avoid full sentences. Separate responses with semicolons. Please make sure your answer is no more than 20 words in total.", # Get the structure of the material.
97
+
98
+ "Perorm a detailed analysis to identify the composition and primary functional components of this material. Ensure they are distinguished from the structure discussed previously; in other words, they should not overlap with answers from earlier structure questions. Provide only the results, without any additional information or explanation. Respond in this format: Composition: YOUR ANSWER; Key Components: YOUR ANSWER. If no such component is identified, state 'N/A'.", # Get the composition and key components (if any) of the material.
99
+
100
+ '''
101
+ 1. Construct a table linking structure, associated mechanisms, and resultant material properties. Consider only the structures and properties outlined in the prerequisites, without altering them when completing the table. The mechanisms described in the paper should always take precedence. Present your findings in a table format with three columns: Structure, Mechanism (Structure & Property), and Property. Include every listed structure and describe the mechanism in detail to prevent ambiguity.
102
+ 2. Please include the effects of environmental conditions on structural characteristics within the mechanisms, if the corresponding property is influenced by environmental conditions.
103
+ 3. To ensure clarity and accuracy in indicating the source of each mechanism (except when N/A), adhere to the following guidelines in your responses: For Mechanisms Directly from the Original Text: If the explanation or mechanism you provide is directly taken or closely paraphrased from the original text, indicate this using the format: "XXX (From text)". For Mechanisms Derived from Your Own Knowledge Base: If the explanation or mechanism is not found in the original text and instead comes from your understanding or external knowledge, use the format: "XXX (From knowledge base)". Please strictly follow this format for each mechanism you explain.
104
+ ''', # Get the mechanism between the target property and the structure.
105
+
106
+ '''Only focus on section B, list the key processing or manufacturing techniques explicitly stated in the text provided for this material; do not base your answers on typical experience. Avoid mentioning details about micro/macro structures. Use semicolons to separate your answers. Additional Requirements: 1. Exclude any processing methods that are hypothetical or proposed as future improvements. Only include those explicitly employed in the current context of the material's manufacturing or processing. 2. Focus solely on manufacturing/synthesis methods, excluding analytical methods like testing and characterization techniques.
107
+ ''', # Get the processing method of the material.
108
+
109
+ '''From the list of processing methods of this material discussed previously, identify the method(s) most central to the paper's discussion. For instance, if process A is highlighted frequently, consider it the primary method. Exclude routine precursor preparation steps unless the paper emphasizes them as critical. Additionally, provide a brief explanation as to why this method, for example, process A, is emphasized in the paper over others, based on the frequency of its mention and its relevance to the key findings or conclusions. This will help to understand the context and significance of the chosen method(s) in the overall study. Ensure to exclude any processing methods not explicitly mentioned in the text.
110
+ RETURN ME STRICTLY WITH THIS FORMAT: The final processing methods are: YOUR ANSWER; The explanations are: YOUR EXPLANATIONS.''', # Refine the processing method of the material.
111
+
112
+ '''
113
+ 1. Construct a table that maps structures to their influential processing methods and the mechanisms responsible for their formation. Frame the relationship as follows: "The [processing method] yields [structure] via the [mechanism]." Ensure a logical and coherent connection among these three elements. The table should consist of three columns titled 'Processing', 'Mechanism (Processing & Structure)', and 'Structure'. Incorporate all relevant structures, omitting those not pertinent to the discussion. Consider only the processing methods specified in the 'processing list'. Each structure must be matched with pertinent processing methods and their mechanisms, emphasizing a clear and logical link among them. The table's content should be concise, focusing exclusively on the interplay between processing methods, mechanisms, and structures.
114
+ 2. For clarity and precision in citing the origin of each mechanism (except when not applicable), adhere to the guidelines below in your responses: For mechanisms directly quoted or closely paraphrased from the original text, use the format: "XXX (From text)". For mechanisms derived from your own knowledge or external sources, use the format: "XXX (From knowledge base)". Strictly follow this format for every mechanism explanation provided.''', # Get the mechanism between the structure and the processing.
115
+
116
+ "Review the provided list that details the composition of the material, including any special component additions. Analyze and identify any components in the list that significantly influence the material's structure. If any such components are identified, add them to the 'Processing' column of the final table's corresponding row. Please do not overwrite the content in the 'Processing' column of the original table unless it is marked as N/A, and refrain from creating a new column for composition/key components.", # Try to add composition and key components into the Processing column.
117
+
118
+ '''Merge the two extracted tables into a single comprehensive table, with the 'Structure' column serving as the primary criterion for merging. Here are the specific instructions:
119
+ 1. Merging Based on Structure: Combine the tables into one, aligning entries based on similarities or matches in the 'Structure' column.
120
+ 2. Handling Non-Matching Structures: For entries in the 'Structure' column that do not match, analyze their relevance and importance based on the context provided by the source papers.
121
+ 3. Decision on Exclusion: After analysis, decide whether the non-matching structure entries are essential to the merged table's overall content and purpose. If not, consider removing these entries to maintain coherence and relevance.
122
+ 4. Columns of the final table: Include Processing, Mechanism (Processing & Structure), Structure, Mechanism (Structure & Property), and Property.
123
+ 5. Ensure the final table is complete, with no gaps. Adjust content as needed for accuracy and consistency with the original texts.
124
+ 6. Mark all uncertain positions as 'N/A'. Replace all blank positions with 'N/A'.
125
+ 7. Prune the table: Examine each row; remove rows with three or more 'N/A' entries. Keep rows with fewer than three 'N/A' entries. If significant information is lost due to pruning, incorporate essential details into other rows, ensuring data coherence and rationale are preserved.
126
+ 8. After completing the above steps, review the document to assess the content's rationality and identify any extraneous content for removal.
127
+ 9. Only the final, corrected table is required; details about the process are not needed.
128
+ The goal is to produce a unified table that accurately reflects the combined data, focusing on maintaining relevance and coherence, especially regarding the 'Structure' column.''' # Merge the two tables.
129
+ ]
130
+
131
+ # ---- For system chart normalization ----
132
+
133
+ PROMPTS_SYSTEM_CHART_NORMALIZATION = [
134
+ '''Several structures have been extracted from a table. Categorize only those structures into groups based on their mechanistic relationships in materials science. Your categorization should be based on the table. Only provide the results and do not include any explanations.
135
+ structures: ''',
136
+
137
+ '''
138
+ Task: In the previous step, all structures were categorized. Based on these results, for each structure in the chart, add a tag to represent its category.
139
+ Requirements:
140
+ 1. Do not change any part of the table except for adding the tag.
141
+ 2. The tag should be added in parentheses after the structure name.
142
+ 3. Your result should be a complete tagged chart.
143
+ Example for tag addition (For reference only):
144
+ Before:
145
+ | Processing | Mechanism (Processing & structure) | structure | Mechanism (structure & Property) | Property |
146
+ |------------|-----------------------------------------|----------------|--------------------------------------|----------|
147
+ | Processing #1 | Mechanism linking Processing #1 and structure #1 (From text)| structure #1 | Mechanism linking structure #1 and Property #1 | Property #1 |
148
+ After:
149
+ | Processing | Mechanism (Processing & structure) | structure | Mechanism (structure & Property) | Property |
150
+ |------------|-----------------------------------------|----------------|--------------------------------------|----------|
151
+ | Processing #1 | Mechanism linking Processing #1 and structure #1 (From text)| structure #1 (Category #1) | Mechanism linking structure #1 and Property #1 | Property #1 |
152
+ Replace 'Category #1' with the actual full category name.
153
+ Table to modify:
154
+ ''',
155
+
156
+ '''Please carefully review each row of the table.
157
+ Task:
158
+ 1. If any row contains specific parameters (e.g., "Yield strength of XXX MPa"), simplify them (e.g., "Yield strength").
159
+ 2. Do not remove any compositions or key elements.
160
+ 3. Do not change the mechanisms.
161
+ 4. Do not remove any environmental conditions (e.g., temperature) under 'Property'.
162
+ 5. Unify the expression for similar properties (e.g., 'High yield strength' and 'Exceptional strength').
163
+ 6. Ensure every property starts with a capital letter.
164
+ Table to modify:
165
+ ''',
166
+
167
+ '''Note that the mechanisms listed under "Mechanism (Processing & structure)" might have mixed mechanisms for processing methods and composition/key elements.
168
+ Task:
169
+ 1. Separate the mechanisms for processing methods and for composition/key elements into individual rows. However, if they belong to the same category based on the provided paper, do not separate them.
170
+ 2. Determine whether each mechanism is a processing method or a key element/composition using the provided paper.
171
+ 3. Allocate the mechanisms without significant changes to their content.
172
+ 4. Keep the rest of the table unchanged.
173
+ 5. After splitting, re-evaluate all mechanisms under "Mechanism (Processing & structure)" to tag them as either "(From text)" or "(From knowledge base)" based on their origin.
174
+ Example for splitting (for reference only):
175
+ Before splitting:
176
+ | Processing | Mechanism (Processing & structure) | structure | Mechanism (structure & Property) | Property |
177
+ |------------|-----------------------------------------|----------------|--------------------------------------|----------|
178
+ | Processing #1; Key element addition #1 | Mechanism linking Processing #1 and structure #1; Mechanism linking key element addition #1 and structure #1 (From text)| structure #1 | Mechanism linking structure #1 and Property #1 | Property #1 |
179
+ After splitting:
180
+ | Processing | Mechanism (Processing & structure) | structure | Mechanism (structure & Property) | Property |
181
+ |------------|-----------------------------------------|----------------|--------------------------------------|----------|
182
+ | Processing #1 | Mechanism linking Processing #1 and structure #1 (From knowledge base)| structure #1 | Mechanism linking structure #1 and Property #1 | Property #1 |
183
+ | Key element addition #1 | Mechanism linking key element addition #1 and structure #1 (From text)| structure #1 | Mechanism linking structure #1 and Property #1 | Property #1 |
184
+ Table to modify:''',
185
+
186
+ '''Currently, a single row may contain multiple properties.
187
+ Task:
188
+ 1. Separate each property into a different row while keeping the columns 'Processing', 'Mechanism (Processing & structure)', and 'structure' unchanged.
189
+ 2. For each property, review the mechanisms in "Mechanism (structure & Property)". Determine which mechanisms are relevant to that property based on the content (ignoring degree adverbs) and include all qualified mechanisms (including those related to environmental conditions).
190
+ Example (for reference only):
191
+ Before:
192
+ | Processing | Mechanism (Processing & structure) | structure | Mechanism (structure & Property) | Property |
193
+ |------------|-----------------------------------------|----------------|--------------------------------------|----------|
194
+ | Processing #1| Mechanism linking Processing #1 and structure #1 (From text)| structure #1 | Mechanism #1 linking structure #1 and Property #1; Mechanism #2 linking structure #1 and Property #2 | Property #1; Property #2 |
195
+ After:
196
+ | Processing | Mechanism (Processing & structure) | structure | Mechanism (structure & Property) | Property |
197
+ |------------|-----------------------------------------|----------------|--------------------------------------|----------|
198
+ | Processing #1| Mechanism linking Processing #1 and structure #1 (From text)| structure #1 | Mechanism #1 linking structure #1 and Property #1 | Property #1 |
199
+ | Processing #1| Mechanism linking Processing #1 and structure #1 (From text)| structure #1 | Mechanism #2 linking structure #1 and Property #2 | Property #2 |
200
+ Table to modify:''',
201
+
202
+ '''Task:
203
+ Step 1. Check if any row has 'N/A' in the 'Structure' column.
204
+ Step 2. For rows with 'N/A', compare the 'Processing' and 'Mechanism (Processing & Structure)' values with other rows. If a match is found, replace 'N/A' with the corresponding value.
205
+ Step 3. Repeat for all 'N/A' in 'Structure'.
206
+ Step 4. Next, check for 'N/A' in the 'Property' column.
207
+ Step 5. For rows with 'N/A' in 'Property', compare the 'Structure' and 'Mechanism (Structure & Property)' values with other rows. If a match is found, replace 'N/A' accordingly.
208
+ Step 6. Provide the updated table. Please return only the content of the chart, removing all modifiers and comments (for example, omit phrases like 'the above table').
209
+ Table to modify:'''
210
+ ]
211
+
212
+ # ---- For hypothesis generation ----
213
+
214
+ def create_hypotheses_generation_prompt(material_goal_1, material_goal_2):
215
+ """Create the hypotheses generation prompt with user-specified material design goals."""
216
+ return f'''Background Information: The table you're referring to has been compiled from various system charts found in multiple research papers. Whenever the same paper number appears more than once in the table, it indicates that the entries originate from the same research study. In many materials science challenges, achieving {material_goal_1} requires innovative design approaches. The challenge is to design materials that can maintain {material_goal_2} across multiple demanding criteria simultaneously. This necessitates the synergistic combination of special design principles for superior material properties with relevant deformation mechanisms to find innovative design solutions.
217
+
218
+ Task Objective: The objective of this research is to design materials that exhibit {material_goal_1} while maintaining {material_goal_2}. Please strictly adhere to this theme.
219
+
220
+ Step 1: Begin by thoroughly reviewing each entry in the table. Then, identify some mechanisms that contribute to {material_goal_1}.
221
+
222
+ Step 2: Review each entry again and select some mechanisms targeting {material_goal_2}, which have synergistic effects with the ideas you selected in Step 1. Ideas selected in Step 1 and Step 2 should come from different papers. Synergistic effects occur when scientific discoveries from different papers can be combined to create a greater positive impact than each would produce on its own. These synergistic effects are not just additive but multiplicative.
223
+
224
+ Detailing Synergistic Hypotheses: For each hypothesis you formulate, provide a detailed explanation of the structural features or mechanisms that might interact synergistically to achieve both {material_goal_1} and {material_goal_2}. This explanation should go beyond simply summarizing the findings from the papers, suggesting innovative interactions not directly discussed in the original studies.
225
+
226
+ Citations: When citing specific data or findings from the table, append the source at the end of the sentence using the format '[number]', where '[number]' denotes the corresponding paper number listed in the table.
227
+
228
+ Ensure you provide me with every potential hypothesis hidden in the table, and each hypothesis should thoroughly address the task in over 400 words. Ensure that your review and hypotheses consider the full breadth of research captured in the table, without bias towards any specific range of paper numbers. Each hypothesis should be well-developed and address the task comprehensively.
229
+
230
+ Answer me in this form: Hypothesis #1: Idea selected from Step 1: XXX; Idea selected from Step 2: XXX; Synergistic mechanism: XXX.'''
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ aiofiles
5
+ requests
6
+ rapidfuzz
7
+ chromadb
8
+ langchain-core
9
+ langchain-openai
10
+ langchain-google-genai
11
+ langchain-community
12
+ langchain-text-splitters
13
+ PyPDF2
14
+ pandas
15
+ networkx
16
+ matplotlib
17
+ Pillow
18
+ regex
system_chart_extraction.py ADDED
@@ -0,0 +1,1322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio, re, textwrap, math, copy, os, uuid
2
+ from datetime import datetime
3
+ import pandas as pd
4
+ import networkx as nx
5
+ import matplotlib.pyplot as plt
6
+ import matplotlib.patches as mpatches
7
+ from PIL import Image
8
+ from langchain_openai import ChatOpenAI
9
+ from langchain_core.messages import HumanMessage, AIMessage
10
+ from io import StringIO
11
+ from collections import defaultdict
12
+
13
+ from prompts import PROMPTS_CLASSIFICATION, PROMPTS_EXPERIMENTAL, PROMPTS_COMPUTATIONAL, PROMPTS_MIXED, PROMPTS_SYSTEM_CHART_NORMALIZATION
14
+
15
+ # -----helper functions and variables-----
16
+
17
+ _CATEGORY_COLORS = [
18
+ '#E1BEE7', # Light Purple
19
+ '#F8BBD9', # Light Pink
20
+ '#FFCDD2', # Light Red
21
+ '#FFF9C4', # Light Yellow
22
+ '#B2DFDB', # Light Teal
23
+ '#D7CCC8', # Light Brown
24
+ '#CFD8DC', # Light Blue Grey
25
+ '#FFCCBC', # Light Orange
26
+ '#DCEDC8', # Light Green
27
+ '#B2EBF2' # Light Cyan
28
+ ]
29
+
30
+ def _parse_microstructures(starter_table: str) -> str:
31
+ """
32
+ Extract the 'Structure' column (or 3rd real column if no headers) from a Markdown-like table.
33
+ Handles both:
34
+ 1) No outer pipes, no explicit header separator
35
+ 2) With outer pipes and a '---|---|...' header separator
36
+ Returns a '; '-joined string of non-empty cells from that column.
37
+ """
38
+ # 1) Normalize lines: strip, drop blanks
39
+ lines = [l.strip() for l in str(starter_table).strip().splitlines() if l.strip()]
40
+ if not lines:
41
+ raise ValueError("Empty table text.")
42
+
43
+ # 2) Remove obvious Markdown header separator lines (e.g., | --- | :---: | --- |)
44
+ def is_md_separator(line: str) -> bool:
45
+ # After removing pipes, colons, dashes and spaces, nothing should remain
46
+ return re.sub(r'[\|\-\:\s]', '', line) == '' and ('-' in line or ':' in line) and '|' in line
47
+
48
+ lines = [ln for ln in lines if not is_md_separator(ln)]
49
+
50
+ # 3) Parse with pandas using '|' as delimiter
51
+ text = '\n'.join(lines)
52
+ df = pd.read_csv(StringIO(text), sep='|', engine='python', header=None, dtype=str, keep_default_na=False)
53
+
54
+ # 4) Trim whitespace in all cells
55
+ df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
56
+
57
+ # 5) Drop fully-empty boundary columns caused by leading/trailing pipes
58
+ def col_empty(series):
59
+ s = series.astype(str).str.strip()
60
+ return s.eq('').all()
61
+
62
+ while df.shape[1] > 0 and col_empty(df.iloc[:, 0]):
63
+ df = df.iloc[:, 1:]
64
+ while df.shape[1] > 0 and col_empty(df.iloc[:, -1]):
65
+ df = df.iloc[:, :-1]
66
+
67
+ if df.shape[1] < 1:
68
+ raise ValueError("No columns detected after normalization.")
69
+
70
+ # 6) Detect header row (case-insensitive). If first row contains 'structure', treat it as header.
71
+ header_row = df.iloc[0].astype(str).tolist()
72
+ header_lower = [h.strip().lower() for h in header_row]
73
+ has_header = any(h == 'structure' for h in header_lower)
74
+
75
+ if has_header:
76
+ columns = [re.sub(r'^\*+(.*?)\*+$', r'\1', c or '').strip() for c in header_row]
77
+ df = df.iloc[1:].reset_index(drop=True)
78
+ df.columns = columns
79
+ # Prefer the 'Structure' column by name; fall back to case-insensitive match
80
+ if 'Structure' in df.columns:
81
+ target = df['Structure']
82
+ else:
83
+ # case-insensitive match
84
+ name_map = {c.lower(): c for c in df.columns}
85
+ target = df[name_map.get('structure')]
86
+ else:
87
+ # No header: need at least 3 real columns, use the 3rd
88
+ if df.shape[1] < 3:
89
+ raise ValueError("Not enough columns to extract the 3rd column.")
90
+ target = df.iloc[:, 2]
91
+
92
+ # 7) Clean typical markdown bullets and noise
93
+ def clean_cell(s: str) -> str:
94
+ s = s.strip()
95
+ s = re.sub(r'^\-\s+', '', s) # leading "- "
96
+ s = re.sub(r'\s*\(from text\)\s*$', '', s, flags=re.IGNORECASE)
97
+ return s.strip('| ').strip()
98
+
99
+ col = target.astype(str).map(clean_cell)
100
+
101
+ # 8) Drop empty rows and join
102
+ col = col[col.str.strip() != '']
103
+ if col.empty:
104
+ return ''
105
+
106
+ # Deduplicate while keeping order (optional; drop if you want duplicates)
107
+ seen = set()
108
+ out = []
109
+ for v in col:
110
+ if v not in seen:
111
+ seen.add(v)
112
+ out.append(v)
113
+
114
+ return '; '.join(out)
115
+
116
+ def _wrap_label(text, wrap_limit=20):
117
+ return textwrap.fill(text, width=wrap_limit)
118
+
119
+ def _parse_composition_and_key_components(response_text: str) -> tuple:
120
+ """
121
+ Parse composition and key components from the LLM response.
122
+ Supports variants like:
123
+ - "Composition: ...; Key Components: ..."
124
+ - "Composition: ...; Key Component: ..."
125
+ - "Composition: ...; Key components addition: ..."
126
+ Returns (composition, key_components). Falls back to "N/A" if missing.
127
+ """
128
+ if not response_text:
129
+ return "N/A", "N/A"
130
+
131
+ text = str(response_text).strip()
132
+ composition = "N/A"
133
+ key_components = "N/A"
134
+
135
+ comp_label = re.search(r'Composition\s*:\s*', text, flags=re.IGNORECASE)
136
+ key_label = re.search(r'Key\s+Components?\s*(?:addition)?\s*:\s*', text, flags=re.IGNORECASE)
137
+
138
+ if comp_label:
139
+ comp_start = comp_label.end()
140
+ comp_end = key_label.start() if key_label and key_label.start() > comp_start else len(text)
141
+ comp_value = text[comp_start:comp_end].strip().strip(';')
142
+ composition = comp_value if comp_value else "N/A"
143
+
144
+ if key_label:
145
+ key_value = text[key_label.end():].strip().strip(';')
146
+ key_components = key_value if key_value else "N/A"
147
+
148
+ return composition, key_components
149
+
150
+ def _llm_content_to_text(content) -> str:
151
+ """
152
+ Normalize heterogeneous LLM content payloads into a plain string.
153
+ Handles str, list (content blocks), dict payloads, and fallback objects.
154
+ """
155
+ if content is None:
156
+ return ""
157
+
158
+ if isinstance(content, str):
159
+ return content
160
+
161
+ if isinstance(content, list):
162
+ parts = []
163
+ for item in content:
164
+ if item is None:
165
+ continue
166
+ if isinstance(item, str):
167
+ parts.append(item)
168
+ continue
169
+ if isinstance(item, dict):
170
+ if isinstance(item.get('text'), str):
171
+ parts.append(item['text'])
172
+ continue
173
+ text_block = item.get('text')
174
+ if isinstance(text_block, dict):
175
+ value = text_block.get('value') or text_block.get('text')
176
+ if value is not None:
177
+ parts.append(str(value))
178
+ continue
179
+ if isinstance(item.get('content'), str):
180
+ parts.append(item['content'])
181
+ continue
182
+ parts.append(str(item))
183
+ continue
184
+
185
+ text_attr = getattr(item, 'text', None)
186
+ if isinstance(text_attr, str):
187
+ parts.append(text_attr)
188
+ else:
189
+ parts.append(str(item))
190
+
191
+ return '\n'.join([part for part in parts if part]).strip()
192
+
193
+ if isinstance(content, dict):
194
+ if isinstance(content.get('text'), str):
195
+ return content['text']
196
+ text_block = content.get('text')
197
+ if isinstance(text_block, dict):
198
+ value = text_block.get('value') or text_block.get('text')
199
+ if value is not None:
200
+ return str(value)
201
+ if isinstance(content.get('content'), str):
202
+ return content['content']
203
+ return str(content)
204
+
205
+ return str(content)
206
+
207
+ class _NormalizedResponse:
208
+ """Proxy response object with normalized string `content`."""
209
+ def __init__(self, response):
210
+ self._response = response
211
+ self.content = _llm_content_to_text(getattr(response, 'content', response))
212
+
213
+ def __getattr__(self, name):
214
+ return getattr(self._response, name)
215
+
216
+
217
+ class _NormalizedLLM:
218
+ """LLM wrapper that normalizes `.ainvoke(...).content` to plain text."""
219
+ def __init__(self, llm):
220
+ self._llm = llm
221
+
222
+ async def ainvoke(self, *args, **kwargs):
223
+ response = await self._llm.ainvoke(*args, **kwargs)
224
+ return _NormalizedResponse(response)
225
+
226
+ def __getattr__(self, name):
227
+ return getattr(self._llm, name)
228
+
229
+ def _transform_table_into_list(total_table):
230
+ ps_list, sp_list = [], []
231
+ for i in total_table.splitlines()[2:]: # Skip header
232
+ elements = [elem.strip() for elem in i.split('|') if elem]
233
+ ps_list.append((elements[0], elements[1], elements[2]))
234
+ sp_list.append((elements[2], elements[3], elements[4]))
235
+ return ps_list, sp_list
236
+
237
+ def _initialize_category_colors(G):
238
+ """Extract categories and initialize colors for structure categories"""
239
+ structure_categories = {attr['category'] for node, attr in G.nodes(data=True) if attr['group'] == 'structure'}
240
+ category_colors = {}
241
+ for i, category in enumerate(structure_categories):
242
+ category_colors[category] = _CATEGORY_COLORS[i % len(_CATEGORY_COLORS)]
243
+ return category_colors
244
+
245
+ def _get_text_width(text, fontsize=16):
246
+ """Calculate the width of the wrapped text"""
247
+ fig = plt.figure()
248
+ renderer = fig.canvas.get_renderer()
249
+ text_obj = plt.text(0, 0, text, fontsize=fontsize)
250
+ bbox = text_obj.get_window_extent(renderer=renderer)
251
+ plt.close(fig)
252
+ return bbox.width / fig.dpi # Convert from pixels to inches
253
+
254
+ def _create_main_diagram(G, pos, fig_width, fig_height, save_path, category_colors):
255
+ """Create the main network diagram and save to file"""
256
+ plt.figure(figsize=(fig_width, fig_height)) # Dynamic figsize
257
+ ax = plt.gca()
258
+ pos2 = copy.deepcopy(pos)
259
+
260
+ dpi = ax.figure.get_dpi()
261
+
262
+ # Draw nodes as rectangles
263
+ key_list = list(pos.keys())
264
+ for iii, nnn in enumerate(pos.items()):
265
+ node, (x, y) = nnn
266
+ group = G.nodes[node]['group']
267
+ header = G.nodes[node].get('header', '')
268
+ content = _wrap_label(G.nodes[node].get('content', node), wrap_limit=30)
269
+ header_width = _get_text_width(header, fontsize=16)
270
+ content_width = max([_get_text_width(line, fontsize=20) for line in content.split('\n')])
271
+ width = content_width / 2.8
272
+ height = 60. / dpi * (content.strip('\n').count('\n') + 1) + 0.6
273
+
274
+ # Set color for nodes
275
+ if group == 'structure':
276
+ facecolor = category_colors.get(G.nodes[node]['category'], 'white')
277
+ elif group == 'processing':
278
+ facecolor = '#FFB74D' # Orange
279
+ elif group == 'property':
280
+ facecolor = '#ADD8E6' # Blue
281
+ else:
282
+ facecolor = 'white'
283
+
284
+ rect = mpatches.FancyBboxPatch((x - width / 2, y - height / 2),
285
+ width, height,
286
+ boxstyle="square,pad=0.0",
287
+ edgecolor='black',
288
+ facecolor=facecolor)
289
+ ax.add_patch(rect)
290
+ plt.text(x, y + height / 2 + 36. / dpi / 2, header, fontsize=22, ha='center', va='center', fontweight='bold')
291
+ plt.text(x, y, content, fontsize=22, ha='center', va='center')
292
+
293
+ # Adjust node positions to avoid edges converging to the center
294
+ if group == 'processing':
295
+ pos[key_list[iii]] = (pos[key_list[iii]][0] + width / 2, pos[key_list[iii]][1])
296
+ pos2[key_list[iii]] = (pos2[key_list[iii]][0] + width / 2, pos2[key_list[iii]][1])
297
+ if group == 'property':
298
+ pos[key_list[iii]] = (pos[key_list[iii]][0] - width / 2, pos[key_list[iii]][1])
299
+ pos2[key_list[iii]] = (pos2[key_list[iii]][0] - width / 2, pos2[key_list[iii]][1])
300
+ elif group == 'structure':
301
+ pos[key_list[iii]] = (pos[key_list[iii]][0] + width / 2, pos[key_list[iii]][1])
302
+ pos2[key_list[iii]] = (pos2[key_list[iii]][0] - width / 2, pos2[key_list[iii]][1])
303
+
304
+ # Draw edges
305
+ node_list = list(G.nodes())
306
+ # Separate the edges based on their groups
307
+ processing_edges = [(u, v) for u, v in G.edges() if G.nodes[u]['group'] == 'processing']
308
+ property_edges = [(u, v) for u, v in G.edges() if G.nodes[v]['group'] == 'property']
309
+
310
+ # Draw processing edges using pos2
311
+ nx.draw_networkx_edges(G, pos2, edgelist=processing_edges, ax=ax)
312
+ # Draw property edges using pos
313
+ nx.draw_networkx_edges(G, pos, edgelist=property_edges, ax=ax)
314
+
315
+ # Draw labels for edges
316
+ for edge in G.edges():
317
+ labels = G.edges[edge]['mechanisms']
318
+ unique_labels = list(set(labels))
319
+ combined_label = ', '.join(unique_labels)
320
+ x, y = pos[edge[0]] if G.nodes[edge[0]]['group'] == 'processing' else pos[edge[0]]
321
+ x2, y2 = pos[edge[1]] if G.nodes[edge[1]]['group'] == 'property' else pos2[edge[1]]
322
+ xytext = ((x + x2) / 2, (y + y2) / 2)
323
+ dx = (x2 - x) * 0.05
324
+ dy = (y2 - y) * 0.05
325
+ rot = math.degrees(math.atan2((y2 - y) * 0.23, (x2 - x)))
326
+ plt.text(xytext[0] + dx, xytext[1] + dy, combined_label, size=18, ha='center', va='center', rotation=rot,
327
+ bbox=dict(facecolor='white', alpha=0.7, edgecolor='none', boxstyle='round,pad=0.3'))
328
+
329
+ header_y_position = max(pos.values(), key=lambda x: x[1])[1] + 1
330
+ plt.text(1, header_y_position + 1, 'Processing', fontsize=30, ha='center', va='center', fontweight='bold')
331
+ plt.text(3, header_y_position + 1, 'Microstructure', fontsize=30, ha='center', va='center', fontweight='bold')
332
+ plt.text(5, header_y_position + 1, 'Property', fontsize=30, ha='center', va='center', fontweight='bold')
333
+
334
+ plt.tight_layout()
335
+ plt.axis('off')
336
+ plt.savefig(save_path, format='png', bbox_inches='tight')
337
+ plt.close()
338
+ return save_path
339
+
340
+ def _get_image_width(image_path):
341
+ """Get the width of an image file"""
342
+ with Image.open(image_path) as img:
343
+ return img.width
344
+
345
+ def _add_na_nodes(G, mechanisms, pos):
346
+ """Add N/A nodes to the graph if they exist in edges"""
347
+ na_nodes = set()
348
+ for edge in G.edges():
349
+ if edge[0] == "N/A" or edge[1] == "N/A":
350
+ na_nodes.update(edge)
351
+
352
+ for na_node in na_nodes:
353
+ if na_node == "N/A":
354
+ continue
355
+ if na_node not in G.nodes:
356
+ G.add_node(na_node, group='na', header='N/A', content='N/A')
357
+ pos[na_node] = (1, -1)
358
+
359
+ def _create_legend(mechanisms, category_colors, canvas_width_pixels, save_path):
360
+ """Create a legend for the network diagram"""
361
+ canvas_width_inches = canvas_width_pixels / 100
362
+ fig_height = len(mechanisms) * 0.3 + len(category_colors) * 0.3
363
+ fig, ax = plt.subplots(figsize=(canvas_width_inches, fig_height))
364
+ legend_patches = [mpatches.Patch(color='none', label=f'{code}: {_wrap_label(mech, wrap_limit=150)}')
365
+ for mech, code in sorted(mechanisms.items(), key=lambda item: int(item[1])) if mech != "N/A"]
366
+ category_patches = [mpatches.Patch(color=color, label=f'{category}')
367
+ for category, color in category_colors.items()]
368
+ ax.legend(handles=legend_patches + category_patches, loc='center', fontsize=22)
369
+ plt.axis('off')
370
+ plt.savefig(save_path, bbox_inches='tight')
371
+ plt.close()
372
+ return save_path
373
+
374
+ def _combine_images(image1_path, image2_path, output_path):
375
+ """Combine two images vertically"""
376
+ img1 = Image.open(image1_path)
377
+ img2 = Image.open(image2_path)
378
+ total_width = max(img1.width, img2.width)
379
+ total_height = img1.height + img2.height
380
+ new_img = Image.new('RGB', (total_width, total_height), 'white')
381
+ new_img.paste(img2, ((total_width - img2.width) // 2, 0))
382
+ new_img.paste(img1, ((total_width - img1.width) // 2, img2.height))
383
+ new_img.save(output_path)
384
+ return output_path
385
+
386
+ def _calculate_fig_dimensions(node_counts, max_label_length, min_height=10, height_per_node=2.0, width_per_char=0.2):
387
+ """Calculate figure dimensions based on node counts and label length"""
388
+ max_nodes = max(node_counts)
389
+ fig_height = max(min_height, max_nodes * height_per_node)
390
+ fig_width = max(20, max_label_length * width_per_char)
391
+ return fig_width, fig_height
392
+
393
+ def _get_max_label_length(nodes):
394
+ """Get the maximum label length from a list of nodes"""
395
+ return max(len(label) for label in nodes)
396
+
397
+ def _split_microstructure_label(label):
398
+ """Split microstructure label into name and category"""
399
+ if '(' in label and ')' in label:
400
+ parts = label.rsplit('(', 1)
401
+ microstructure = parts[0].strip()
402
+ category = parts[1].rstrip(')').strip()
403
+ return microstructure, category
404
+ return label, 'Other categories'
405
+
406
+ def _assign_unique_identifiers(ps_mechanisms, sp_mechanisms):
407
+ """Assign unique identifiers to mechanisms"""
408
+ unique_mechanisms = {}
409
+ current_code = 1
410
+ for mech in ps_mechanisms:
411
+ if (mech != "N/A") and (mech not in unique_mechanisms):
412
+ unique_mechanisms[mech] = str(current_code)
413
+ current_code += 1
414
+ for mech in sp_mechanisms:
415
+ if (mech != "N/A") and (mech not in unique_mechanisms):
416
+ unique_mechanisms[mech] = str(current_code)
417
+ current_code += 1
418
+ return unique_mechanisms
419
+
420
+ def _calculate_node_positions(nodes, x_position, vertical_gap, offset_y=0):
421
+ """Calculate positions for nodes in the graph"""
422
+ return {node: (x_position, offset_y + (len(nodes) - 1 - i) * vertical_gap) for i, node in enumerate(nodes)}
423
+
424
+ def create_network_diagram(system_chart, paper_title, temp_dir):
425
+ """
426
+ Main function to create network diagram from system chart
427
+
428
+ Args:
429
+ system_chart: The extracted system chart content
430
+ paper_title: Title of the paper for filename generation
431
+ temp_dir: Temporary directory to save the visualization files
432
+
433
+ Returns:
434
+ str: Path to the combined diagram file
435
+ """
436
+ import os
437
+ import uuid
438
+ import re
439
+
440
+ ps_list, sp_list = _transform_table_into_list(system_chart.strip())
441
+
442
+ ps_mechanisms = [mech for _, mech, _ in ps_list]
443
+ sp_mechanisms = [mech for _, mech, _ in sp_list]
444
+
445
+ unique_mechanisms = _assign_unique_identifiers(ps_mechanisms, sp_mechanisms)
446
+
447
+ G = nx.DiGraph()
448
+ category_to_microstructures = defaultdict(set)
449
+
450
+ for processing, mechanism, structure in ps_list:
451
+ if mechanism != "N/A":
452
+ microstructure, category = _split_microstructure_label(structure)
453
+ category_to_microstructures[category].add(microstructure)
454
+ wrapped_processing = _wrap_label(processing)
455
+ wrapped_microstructure = _wrap_label(microstructure)
456
+ G.add_node(wrapped_processing, group='processing', category='Processing')
457
+ G.add_node(wrapped_microstructure, group='structure', category=category)
458
+ G.nodes[wrapped_microstructure]['header'] = ''
459
+ G.nodes[wrapped_microstructure]['content'] = wrapped_microstructure
460
+ if (wrapped_processing, wrapped_microstructure) in G.edges:
461
+ G.edges[wrapped_processing, wrapped_microstructure]['mechanisms'].append(unique_mechanisms[mechanism])
462
+ else:
463
+ G.add_edge(wrapped_processing, wrapped_microstructure, mechanisms=[unique_mechanisms[mechanism]])
464
+
465
+ for structure, mechanism, property in sp_list:
466
+ if mechanism != "N/A":
467
+ microstructure, category = _split_microstructure_label(structure)
468
+ wrapped_property = _wrap_label(property)
469
+ wrapped_microstructure = _wrap_label(microstructure)
470
+ G.add_node(wrapped_microstructure, group='structure', category=category)
471
+ G.nodes[wrapped_microstructure]['header'] = ''
472
+ G.nodes[wrapped_microstructure]['content'] = wrapped_microstructure
473
+ G.add_node(wrapped_property, group='property', category='Property')
474
+ if (wrapped_microstructure, wrapped_property) in G.edges:
475
+ G.edges[wrapped_microstructure, wrapped_property]['mechanisms'].append(unique_mechanisms[mechanism])
476
+ else:
477
+ G.add_edge(wrapped_microstructure, wrapped_property, mechanisms=[unique_mechanisms[mechanism]])
478
+
479
+ # Define node positions before calling _add_na_nodes
480
+ processing_nodes = [node for node, attr in G.nodes(data=True) if attr['group'] == 'processing']
481
+ structure_nodes = [node for node, attr in G.nodes(data=True) if attr['group'] == 'structure']
482
+ property_nodes = [node for node, attr in G.nodes(data=True) if attr['group'] == 'property']
483
+
484
+ # Calculate the offsets to center the processing and property nodes
485
+ processing_height = len(processing_nodes) * 3.0
486
+ structure_height = len(structure_nodes) * 2.5
487
+ property_height = len(property_nodes) * 3.0
488
+
489
+ offset_y_processing = (structure_height - processing_height) / 2.5 if structure_height > processing_height else 0
490
+ offset_y_property = (structure_height - property_height) / 2.5 if structure_height > property_height else 0
491
+
492
+ processing_positions = _calculate_node_positions(processing_nodes, 1, 2.5, offset_y_processing)
493
+ structure_positions = _calculate_node_positions(structure_nodes, 3, 2)
494
+ property_positions = _calculate_node_positions(property_nodes, 5, 2.5, offset_y_property)
495
+
496
+ pos = {**processing_positions, **structure_positions, **property_positions}
497
+
498
+ max_label_length = _get_max_label_length(G.nodes)
499
+ fig_width, fig_height = _calculate_fig_dimensions([len(processing_nodes), len(structure_nodes), len(property_nodes)], max_label_length)
500
+
501
+ # Initialize category colors for structure nodes
502
+ category_colors = _initialize_category_colors(G)
503
+
504
+ # Add N/A nodes after defining pos
505
+ _add_na_nodes(G, unique_mechanisms, pos)
506
+
507
+ # Create safe filename from paper title
508
+ safe_title = re.sub(r'[^\w\s-]', '', paper_title)[:50] # Remove special chars, limit length
509
+ safe_title = re.sub(r'[-\s]+', '_', safe_title) # Replace spaces/hyphens with underscores
510
+ unique_id = str(uuid.uuid4())[:8]
511
+
512
+ # Create file paths
513
+ diagram_path = os.path.join(temp_dir, f"diagram_{safe_title}_{unique_id}.png")
514
+ legend_path = os.path.join(temp_dir, f"legend_{safe_title}_{unique_id}.png")
515
+ combined_path = os.path.join(temp_dir, f"system_chart_{safe_title}_{unique_id}.png")
516
+
517
+ # Create the diagrams
518
+ _create_main_diagram(G, pos, fig_width, fig_height, diagram_path, category_colors)
519
+ diagram_img_width = _get_image_width(diagram_path)
520
+ _create_legend(unique_mechanisms, category_colors, diagram_img_width, legend_path)
521
+ _combine_images(diagram_path, legend_path, combined_path)
522
+
523
+ # Clean up intermediate files
524
+ try:
525
+ os.remove(diagram_path)
526
+ os.remove(legend_path)
527
+ except OSError:
528
+ pass # Don't fail if cleanup doesn't work
529
+
530
+ return combined_path
531
+
532
+
533
+ def sanitize_filename(filename: str) -> str:
534
+ """Sanitize filename by removing/replacing invalid characters"""
535
+ import re
536
+ # Remove or replace invalid characters
537
+ filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
538
+ # Limit length
539
+ return filename[:100] if len(filename) > 100 else filename
540
+
541
+ async def openai_api_function(input_list, llm):
542
+ """
543
+ Simple wrapper for OpenAI API calls using langchain
544
+
545
+ Args:
546
+ input_list: List of previous conversation messages
547
+ llm: ChatOpenAI instance
548
+
549
+ Returns:
550
+ tuple: (updated input_list, latest response)
551
+ """
552
+ from langchain_core.messages import HumanMessage, AIMessage
553
+
554
+ # Convert input_list to proper message format if needed
555
+ messages = []
556
+ for i, content in enumerate(input_list):
557
+ if i % 2 == 0: # Human messages (even indices)
558
+ messages.append(HumanMessage(content=content))
559
+ else: # AI messages (odd indices)
560
+ messages.append(AIMessage(content=content))
561
+
562
+ # Get response from LLM
563
+ response = await llm.ainvoke(messages)
564
+ response_text = _llm_content_to_text(getattr(response, 'content', response))
565
+
566
+ # Return updated list and response content
567
+ return input_list, response_text
568
+
569
+ def _visualize_one_system_chart(system_chart_table_text: str, paper_text: str = "", citation: str = "", llm=None, temp_dir: str = "/tmp"):
570
+ """
571
+ Visualize system chart for a single paper, saving images in a citation-named subfolder.
572
+
573
+ Args:
574
+ system_chart_table_text: The system chart table as text
575
+ paper_text: Full paper text for context
576
+ citation: Citation/title for the paper
577
+ llm: Language model instance
578
+ temp_dir: Directory to save visualizations
579
+
580
+ Returns:
581
+ str: Path to the created visualization
582
+ """
583
+ import asyncio
584
+
585
+ async def _async_visualize():
586
+ print("Processing system chart...")
587
+
588
+ # Parse the table to extract microstructures
589
+ try:
590
+ df = pd.read_csv(StringIO(system_chart_table_text), delimiter='|', skipinitialspace=True).iloc[1:, 1:-1].reset_index(drop=True)
591
+ df.columns = df.columns.str.strip()
592
+ microstructures = df['Microstructure'].str.strip().tolist()
593
+ microstructures = '; '.join(microstructures)
594
+ except Exception as e:
595
+ print(f"Error parsing table: {e}")
596
+ microstructures = "N/A"
597
+
598
+ table_string = (
599
+ "The table is as follows:\n"
600
+ f" The table: {system_chart_table_text}"
601
+ )
602
+
603
+ input_list = []
604
+ r_last = None
605
+
606
+ # Use the normalization prompts as our chart-to-diagram prompts
607
+ prompts_chart_to_diagram = PROMPTS_SYSTEM_CHART_NORMALIZATION
608
+
609
+ for i in range(len(prompts_chart_to_diagram)):
610
+ print(f"Processing step {i+1}/{len(prompts_chart_to_diagram)}...")
611
+
612
+ if i == 0:
613
+ prompt = prompts_chart_to_diagram[i].format(microstructures=microstructures)
614
+ if paper_text:
615
+ prompt = (
616
+ f"{prompt}\n\n"
617
+ "-----\n"
618
+ "Full paper context (for reference only):\n"
619
+ f"{paper_text}"
620
+ )
621
+ elif i == 1:
622
+ prompt = prompts_chart_to_diagram[i] + table_string
623
+ else:
624
+ prompt = prompts_chart_to_diagram[i].format(r=r_last)
625
+
626
+ input_list.append(prompt)
627
+
628
+ if llm:
629
+ input_list, r = await openai_api_function(input_list, llm)
630
+ input_list.append(r)
631
+ r_last = r
632
+ else:
633
+ print("Warning: No LLM provided, skipping API call")
634
+ r_last = system_chart_table_text # Use original table as fallback
635
+
636
+ print("Creating diagrams...")
637
+
638
+ # Save in visualized_system_charts/citation/
639
+ base_folder = os.path.join(temp_dir, 'visualized_system_charts')
640
+ os.makedirs(base_folder, exist_ok=True)
641
+
642
+ safe_citation = sanitize_filename(citation) if citation else "unknown_citation"
643
+ subfolder_path = os.path.join(base_folder, safe_citation)
644
+ os.makedirs(subfolder_path, exist_ok=True)
645
+
646
+ # Create the network diagram
647
+ try:
648
+ visualization_path = create_network_diagram(r_last or system_chart_table_text, citation, subfolder_path)
649
+ print("Diagrams created!")
650
+ return visualization_path
651
+ except Exception as e:
652
+ print(f"Error creating diagram: {e}")
653
+ return None
654
+
655
+ # Run the async function
656
+ return asyncio.run(_async_visualize())
657
+
658
+ async def system_chart_extraction(paper_dict: dict, llm: ChatOpenAI, progress_callback=None, enable_visualization=False, temp_dir=None, extraction_id=None, run_normalization: bool = False) -> tuple:
659
+ """
660
+ Extract system charts from papers with detailed progress tracking.
661
+
662
+ Args:
663
+ paper_dict: Dictionary of title -> paper content
664
+ llm: Language model for processing
665
+ progress_callback: Function to call with progress updates
666
+ enable_visualization: Whether to create visualizations
667
+ temp_dir: Temporary directory to save visualization files
668
+ """
669
+ # Normalize LLM output at the boundary so downstream logic always receives strings.
670
+ llm = _NormalizedLLM(llm)
671
+
672
+ # Initialize output with header and format information
673
+ output_system_charts = ''''''
674
+
675
+ output_all_details = ''''''
676
+ created_visualizations = [] # Track created visualization files
677
+
678
+ total_papers = len(paper_dict)
679
+
680
+ # Initialize progress
681
+ if progress_callback:
682
+ progress_callback({
683
+ "status": "initializing",
684
+ "progress": 0,
685
+ "total_papers": total_papers,
686
+ "current_paper": 0,
687
+ "message": "Initializing..."
688
+ })
689
+
690
+ # First pass: classify all papers to calculate total questions
691
+ paper_classifications = {}
692
+ questions_per_type = {
693
+ 'Experimental/Practical': 9,
694
+ 'Theoretical/Computational': 6,
695
+ 'Mixed': 9
696
+ }
697
+
698
+ for idx, (title, paper) in enumerate(paper_dict.items()):
699
+ if progress_callback:
700
+ progress_callback({
701
+ "status": "classifying paper type",
702
+ "progress": int((idx / total_papers) * 20), # Classification takes 20% of total
703
+ "total_papers": total_papers,
704
+ "current_paper": idx + 1,
705
+ "message": f"Classifying paper {idx + 1}/{total_papers}: {title[:50]}..."
706
+ })
707
+
708
+ # Add small delay to allow frontend to catch up
709
+ await asyncio.sleep(0.5)
710
+
711
+ # Classify paper
712
+ conversation_history = [HumanMessage(content=PROMPTS_CLASSIFICATION + '\n' + paper)]
713
+ response = await llm.ainvoke(conversation_history)
714
+ classification_result = response.content
715
+
716
+ # Yield control back to event loop to allow other requests to be processed
717
+ await asyncio.sleep(0)
718
+
719
+ try:
720
+ type_of_paper = classification_result.split('Reasons: ')[0].strip().split('Category: ')[1].strip()
721
+ # Normalize the classification result
722
+ if type_of_paper not in questions_per_type:
723
+ type_of_paper = 'Mixed' # Default fallback
724
+ except:
725
+ type_of_paper = 'Mixed' # Default fallback if parsing fails
726
+
727
+ paper_classifications[title] = type_of_paper
728
+
729
+ # Calculate total questions (classification + actual questions + normalization)
730
+ total_questions = total_papers # One classification per paper
731
+ for paper_type in paper_classifications.values():
732
+ total_questions += questions_per_type[paper_type]
733
+
734
+ effective_normalization_steps = len(PROMPTS_SYSTEM_CHART_NORMALIZATION)
735
+ if run_normalization:
736
+ total_questions += effective_normalization_steps * total_papers # Normalization steps
737
+
738
+ completed_questions = total_papers # We've completed all classifications
739
+
740
+ # Update progress after classification phase is complete
741
+ if progress_callback:
742
+ progress_callback({
743
+ "status": "classification_complete",
744
+ "progress": 20,
745
+ "total_papers": total_papers,
746
+ "total_questions": total_questions,
747
+ "completed_questions": completed_questions,
748
+ "message": f"Classification complete. Starting to process {total_questions} total questions across {total_papers} papers..."
749
+ })
750
+
751
+ # Second pass: process each paper
752
+ count_for_papers = 1
753
+
754
+ for title, paper in paper_dict.items():
755
+ paper_type = paper_classifications[title]
756
+
757
+ # Called once per paper at the start of the paper processing, high level
758
+ if progress_callback:
759
+ progress_callback({
760
+ "status": "starting paper processing",
761
+ "progress": int((completed_questions / total_questions) * 100),
762
+ "total_papers": total_papers,
763
+ "current_paper": count_for_papers,
764
+ "total_questions": total_questions,
765
+ "completed_questions": completed_questions,
766
+ "current_paper_type": paper_type,
767
+ "phase": "starting",
768
+ "message": f"Starting paper {count_for_papers}/{total_papers} ({paper_type}): {title[:50]}..."
769
+ })
770
+
771
+ # Reset for each paper
772
+ count_for_questions = 0
773
+ count_for_visualization = 0
774
+ conversation_history = []
775
+ conversation_history_for_visualization = []
776
+ five_answers_summary = {}
777
+
778
+ # Add classification result to details
779
+ output_all_details += ('\n')
780
+ output_all_details += ('='*100 + '\n')
781
+ output_all_details += (title)
782
+ output_all_details += ('\n' + '='*100 + '\n')
783
+ output_all_details += ('Check the type of the paper (Experimental/Practical, Theoretical/Computational or Mixed), this paper belongs to: ')
784
+ output_all_details += (' '*5)
785
+ output_all_details += (paper_type + '\n')
786
+ output_all_details += ('\n')
787
+
788
+ # Re-establish conversation history with classification
789
+ conversation_history.append(HumanMessage(content=PROMPTS_CLASSIFICATION + '\n' + paper))
790
+ classification_response = f"Category: {paper_type}\nReasons: Paper classified as {paper_type} based on content analysis."
791
+ conversation_history.append(AIMessage(content=classification_response))
792
+
793
+ def update_question_progress(question_num, if_visualization=False):
794
+ nonlocal completed_questions
795
+ completed_questions += 1
796
+ if not if_visualization:
797
+ if progress_callback:
798
+ progress_callback({
799
+ "status": "extracting system chart",
800
+ "progress": int((completed_questions / total_questions) * 100),
801
+ "total_papers": total_papers,
802
+ "current_paper": count_for_papers,
803
+ "total_questions": total_questions,
804
+ "completed_questions": completed_questions,
805
+ "current_paper_type": paper_type,
806
+ "current_question": question_num + 1,
807
+ "questions_in_paper": questions_per_type[paper_type],
808
+ "phase": "extraction",
809
+ "message": f"Paper {count_for_papers}/{total_papers} - Extracting system chart (Q{question_num + 1}/{questions_per_type[paper_type]})"
810
+ })
811
+ else:
812
+ if progress_callback:
813
+ # Provide specific status messages for different normalization steps
814
+ step_descriptions = {
815
+ 0: "categorizing microstructures",
816
+ 1: "standardizing properties",
817
+ 2: "splitting mechanisms",
818
+ 3: "separating multi-properties",
819
+ 4: "filling missing values",
820
+ 5: "enforcing terminology consistency"
821
+ }
822
+ step_desc = step_descriptions.get(question_num, "normalizing system chart")
823
+ effective_normalization_steps = len(PROMPTS_SYSTEM_CHART_NORMALIZATION)
824
+
825
+ progress_callback({
826
+ "status": step_desc,
827
+ "progress": int((completed_questions / total_questions) * 100),
828
+ "total_papers": total_papers,
829
+ "current_paper": count_for_papers,
830
+ "total_questions": total_questions,
831
+ "completed_questions": completed_questions,
832
+ "phase": "normalization",
833
+ "current_normalization_step": question_num + 1,
834
+ "total_normalization_steps": effective_normalization_steps,
835
+ "message": f"Paper {count_for_papers}/{total_papers} - Normalization: {step_desc} (Step {question_num + 1}/{effective_normalization_steps})"
836
+ })
837
+
838
+ # Process questions based on paper type
839
+ if paper_type == 'Experimental/Practical':
840
+ prompts = PROMPTS_EXPERIMENTAL
841
+ max_questions = 9
842
+ elif paper_type == 'Theoretical/Computational':
843
+ prompts = PROMPTS_COMPUTATIONAL
844
+ max_questions = 6
845
+ else: # Mixed
846
+ prompts = PROMPTS_MIXED
847
+ max_questions = 9
848
+
849
+ # Process all questions for this paper
850
+ final_result = "" # Initialize final_result
851
+ while count_for_questions < max_questions:
852
+ if paper_type == 'Experimental/Practical':
853
+ r, sp, ps, output_all_details = await process_experimental_question(count_for_questions, prompts, conversation_history,
854
+ llm, five_answers_summary, output_all_details, count_for_papers)
855
+ elif paper_type == 'Theoretical/Computational':
856
+ r, sp, ps, output_all_details = await process_computational_question(count_for_questions, prompts, conversation_history,
857
+ llm, five_answers_summary, output_all_details, count_for_papers)
858
+ else: # Mixed
859
+ r, sp, ps, output_all_details = await process_mixed_question(count_for_questions, prompts, conversation_history,
860
+ llm, five_answers_summary, output_all_details, count_for_papers)
861
+
862
+ # Store the result for the final system chart
863
+ final_result = r
864
+
865
+ # Update progress after each question
866
+ update_question_progress(count_for_questions, if_visualization=False)
867
+
868
+ count_for_questions += 1
869
+
870
+ # Run normalization only when requested (visualization implies normalization).
871
+ normalized_chart = None
872
+ if final_result and run_normalization:
873
+ # Notify that normalization phase is starting
874
+ if progress_callback:
875
+ progress_callback({
876
+ "status": "starting normalization",
877
+ "progress": int((completed_questions / total_questions) * 100),
878
+ "total_papers": total_papers,
879
+ "current_paper": count_for_papers,
880
+ "total_questions": total_questions,
881
+ "completed_questions": completed_questions,
882
+ "phase": "normalization_start",
883
+ "message": f"Paper {count_for_papers}/{total_papers} - Starting normalization..."
884
+ })
885
+
886
+ try:
887
+ raw_chart = final_result
888
+ normalized_chart = final_result
889
+ print(f"\n🚀 Starting normalization process for paper: {title}")
890
+ print(f"📊 Initial system chart length: {len(final_result)} characters")
891
+ print("=" * 80)
892
+
893
+ # Process all normalization steps (always execute, not just for visualization)
894
+ effective_steps = len(PROMPTS_SYSTEM_CHART_NORMALIZATION)
895
+ print(f"🔧 Total normalization steps to execute: {effective_steps}")
896
+
897
+ while count_for_visualization < effective_steps:
898
+ print(f"\n⏳ Starting normalization step {count_for_visualization + 1}/{effective_steps}")
899
+
900
+ normalized_chart = await normalize_system_charts(
901
+ count_for_visualization,
902
+ PROMPTS_SYSTEM_CHART_NORMALIZATION,
903
+ conversation_history_for_visualization,
904
+ normalized_chart,
905
+ raw_chart,
906
+ paper,
907
+ llm
908
+ )
909
+
910
+ print(f"📊 Chart length after step {count_for_visualization + 1}: {len(normalized_chart)} characters")
911
+
912
+ # Update progress after each normalization step
913
+ update_question_progress(count_for_visualization, if_visualization=True)
914
+ count_for_visualization += 1
915
+
916
+ print(f"\n✅ Normalization process completed! Final chart length: {len(normalized_chart)} characters")
917
+ print("=" * 80)
918
+ print("📋 FINAL NORMALIZED SYSTEM CHART:")
919
+ print("-" * 60)
920
+ print(normalized_chart[:1000] + "..." if len(normalized_chart) > 1000 else normalized_chart)
921
+ print("-" * 60)
922
+ print("=" * 80)
923
+
924
+ # Keep the ORIGINAL final_result for system_charts.txt output
925
+ # (normalized_chart is only used for visualization)
926
+
927
+ # Optional: Create visualization if requested
928
+ if enable_visualization and temp_dir:
929
+ visualization_path = await create_visualization(
930
+ normalized_chart,
931
+ title,
932
+ temp_dir,
933
+ created_visualizations
934
+ )
935
+
936
+ if visualization_path:
937
+ print(f"✅ Visualization completed for paper: {title}")
938
+ else:
939
+ print(f"⚠️ Visualization creation failed for paper: {title}")
940
+
941
+ except Exception as e:
942
+ print(f"❌ Normalization failed for paper '{title}': {str(e)}")
943
+ # Continue processing other papers even if normalization fails
944
+
945
+ # Add final result to system charts with new format
946
+ delimiter = "=" * 100
947
+ output_system_charts += f'\n{delimiter}\n'
948
+ output_system_charts += f'PAPER: {title}\n'
949
+ output_system_charts += f'SYSTEM_CHART: {final_result}\n'
950
+ output_system_charts += f'{delimiter}\n\n'
951
+
952
+ # Save conversation for this paper if extraction_id is provided
953
+ if extraction_id:
954
+ try:
955
+ from conversation_storage import save_conversation
956
+ conversation_key = save_conversation(
957
+ extraction_id=extraction_id,
958
+ paper_title=title,
959
+ conversation_history=conversation_history,
960
+ paper_content=paper,
961
+ paper_type=paper_type
962
+ )
963
+ print(f"💾 Saved conversation for paper: {title} (key: {conversation_key})")
964
+ except Exception as e:
965
+ print(f"⚠️ Could not save conversation for paper '{title}': {str(e)}")
966
+
967
+ count_for_papers += 1
968
+
969
+ # Final progress update
970
+ if progress_callback:
971
+ progress_callback({
972
+ "status": "completed",
973
+ "progress": 100,
974
+ "total_papers": total_papers,
975
+ "total_questions": total_questions,
976
+ "completed_questions": completed_questions,
977
+ "message": f"System chart extraction completed! Processed {total_papers} papers with {total_questions} total questions."
978
+ })
979
+
980
+ return output_system_charts, output_all_details, created_visualizations
981
+
982
+ async def process_experimental_question(count_for_questions, prompts, conversation_history, llm,
983
+ five_answers_summary, output_all_details, count_for_papers):
984
+ """Process a single question for experimental papers"""
985
+ sp, ps = None, None
986
+ r = "" # Initialize r
987
+
988
+ if count_for_questions == 0:
989
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
990
+ response = await llm.ainvoke(conversation_history)
991
+ conversation_history.append(AIMessage(content=response.content))
992
+ await asyncio.sleep(0)
993
+ r = response.content
994
+ five_answers_summary['property'] = r.split(';')
995
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions} : Properties are: ' + r + '\n')
996
+ elif count_for_questions == 1:
997
+ output_all_details += ('~'*100 + '\n')
998
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
999
+ response = await llm.ainvoke(conversation_history)
1000
+ conversation_history.append(AIMessage(content=response.content))
1001
+ await asyncio.sleep(0)
1002
+ r = response.content
1003
+ five_answers_summary['microstructure'] = r.split(';')
1004
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: Microstructures are: '+ r + '\n')
1005
+ elif count_for_questions == 2:
1006
+ output_all_details += ('~'*100 + '\n')
1007
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1008
+ response = await llm.ainvoke(conversation_history)
1009
+ conversation_history.append(AIMessage(content=response.content))
1010
+ await asyncio.sleep(0)
1011
+ r = response.content
1012
+ composition, key_elements = _parse_composition_and_key_components(r)
1013
+ five_answers_summary['composition&key_elements'] = composition
1014
+ if 'N/A' not in key_elements:
1015
+ five_answers_summary['composition&key_elements'] += '; ' + key_elements
1016
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: ' + r + '\n')
1017
+ elif count_for_questions == 3:
1018
+ output_all_details += ('~'*100 + '\n')
1019
+ prerequisites = f"Prerequisites: Enhanced properties of the alloy: {five_answers_summary['property']}; Microstructure of the alloy: {five_answers_summary['microstructure']}. "
1020
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1021
+ response = await llm.ainvoke(conversation_history)
1022
+ conversation_history.append(AIMessage(content=response.content))
1023
+ await asyncio.sleep(0)
1024
+ r = response.content
1025
+ sp = r
1026
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1027
+ elif count_for_questions == 4:
1028
+ output_all_details += ('~'*100 + '\n')
1029
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1030
+ response = await llm.ainvoke(conversation_history)
1031
+ conversation_history.append(AIMessage(content=response.content))
1032
+ await asyncio.sleep(0)
1033
+ r = response.content
1034
+ five_answers_summary['processing'] = r.split(';')
1035
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}' + ' Processing methods are: ' + r + '\n')
1036
+ elif count_for_questions == 5:
1037
+ output_all_details += ('~'*100 + '\n')
1038
+ prerequisites = f"Processing methods previously discussed: {r}. "
1039
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1040
+ response = await llm.ainvoke(conversation_history)
1041
+ conversation_history.append(AIMessage(content=response.content))
1042
+ await asyncio.sleep(0)
1043
+ r = response.content
1044
+ five_answers_summary['processing'] = r.split('The explanations are:')[0].split(': ')[1]
1045
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1046
+ elif count_for_questions == 6:
1047
+ output_all_details += ('~'*100 + '\n')
1048
+ prerequisites = f"Prerequisites: Processing methods of the alloy: {five_answers_summary['processing']}; Microstructure of the alloy: {five_answers_summary['microstructure']}. "
1049
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1050
+ response = await llm.ainvoke(conversation_history)
1051
+ conversation_history.append(AIMessage(content=response.content))
1052
+ await asyncio.sleep(0)
1053
+ r = response.content
1054
+ output_all_details += (f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1055
+ elif count_for_questions == 7:
1056
+ output_all_details += ('~'*100 + '\n')
1057
+ prerequisites = f"Prerequisites: Composition & Key components (if any) of the alloy: {five_answers_summary['composition&key_elements']}; Microstructure of the alloy: {five_answers_summary['microstructure']}. "
1058
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1059
+ response = await llm.ainvoke(conversation_history)
1060
+ conversation_history.append(AIMessage(content=response.content))
1061
+ await asyncio.sleep(0)
1062
+ r = response.content
1063
+ ps = r
1064
+ output_all_details += (f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1065
+ elif count_for_questions == 8:
1066
+ output_all_details += ('~'*100 + '\n')
1067
+ prerequisites = f"Prerequisites: First table for relationsip between processing and microstructure: {ps}; Second table for relationsip between microstructure and properties: {sp}. "
1068
+ conversation_history.append(HumanMessage(content=prerequisites + '\n' + prompts[count_for_questions]))
1069
+ response = await llm.ainvoke(conversation_history)
1070
+ conversation_history.append(AIMessage(content=response.content))
1071
+ await asyncio.sleep(0)
1072
+ r = response.content
1073
+ output_all_details += ('\n' + f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1074
+
1075
+ return r, sp, ps, output_all_details
1076
+
1077
+ async def process_computational_question(count_for_questions, prompts, conversation_history, llm,
1078
+ five_answers_summary, output_all_details, count_for_papers=1):
1079
+ """Process a single question for computational papers"""
1080
+ sp, ps = None, None
1081
+ r = "" # Initialize r
1082
+
1083
+ if count_for_questions == 0:
1084
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1085
+ response = await llm.ainvoke(conversation_history)
1086
+ conversation_history.append(AIMessage(content=response.content))
1087
+ await asyncio.sleep(0)
1088
+ r = response.content
1089
+ five_answers_summary['property'] = r.split(';')
1090
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions} : Properties are: ' + r + '\n')
1091
+ elif count_for_questions == 1:
1092
+ output_all_details += ('~'*100 + '\n')
1093
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1094
+ response = await llm.ainvoke(conversation_history)
1095
+ conversation_history.append(AIMessage(content=response.content))
1096
+ await asyncio.sleep(0)
1097
+ r = response.content
1098
+ five_answers_summary['microstructure'] = r.split(';')
1099
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: Microstructures are: '+ r + '\n')
1100
+ elif count_for_questions == 2:
1101
+ output_all_details += ('~'*100 + '\n')
1102
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1103
+ response = await llm.ainvoke(conversation_history)
1104
+ conversation_history.append(AIMessage(content=response.content))
1105
+ await asyncio.sleep(0)
1106
+ r = response.content
1107
+ composition, key_elements = _parse_composition_and_key_components(r)
1108
+ five_answers_summary['composition&key_elements'] = composition
1109
+ if 'N/A' not in key_elements:
1110
+ five_answers_summary['composition&key_elements'] += '; ' + key_elements
1111
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: ' + r + '\n')
1112
+ elif count_for_questions == 3:
1113
+ output_all_details += ('~'*100 + '\n')
1114
+ prerequisites = f"Prerequisites: Enhanced properties of the alloy: {five_answers_summary['property']}; Microstructure of the alloy: {five_answers_summary['microstructure']}."
1115
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1116
+ response = await llm.ainvoke(conversation_history)
1117
+ conversation_history.append(AIMessage(content=response.content))
1118
+ await asyncio.sleep(0)
1119
+ r = response.content
1120
+ sp = r
1121
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1122
+ elif count_for_questions == 4:
1123
+ output_all_details += ('~'*100 + '\n')
1124
+ prerequisites = f"Prerequisites: Composition & Key components of the alloy: {five_answers_summary['composition&key_elements']}; Microstructure of the alloy: {five_answers_summary['microstructure']}."
1125
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1126
+ response = await llm.ainvoke(conversation_history)
1127
+ conversation_history.append(AIMessage(content=response.content))
1128
+ await asyncio.sleep(0)
1129
+ r = response.content
1130
+ ps = r
1131
+ output_all_details += (f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1132
+ elif count_for_questions == 5:
1133
+ output_all_details += ('~'*100 + '\n')
1134
+ prerequisites = f"Prerequisites: First table for relationsip between processing and microstructure: {ps}; Second table for relationsip between microstructure and properties: {sp}. "
1135
+ conversation_history.append(HumanMessage(content=prerequisites + '\n' + prompts[count_for_questions]))
1136
+ response = await llm.ainvoke(conversation_history)
1137
+ conversation_history.append(AIMessage(content=response.content))
1138
+ await asyncio.sleep(0)
1139
+ r = response.content
1140
+ output_all_details += ('\n' + f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1141
+
1142
+ return r, sp, ps, output_all_details
1143
+
1144
+ async def process_mixed_question(count_for_questions, prompts, conversation_history, llm,
1145
+ five_answers_summary, output_all_details, count_for_papers):
1146
+ """Process a single question for mixed papers"""
1147
+ sp, ps = None, None
1148
+ r = "" # Initialize r
1149
+
1150
+ if count_for_questions == 0:
1151
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1152
+ response = await llm.ainvoke(conversation_history)
1153
+ conversation_history.append(AIMessage(content=response.content))
1154
+ await asyncio.sleep(0)
1155
+ r = response.content
1156
+ five_answers_summary['property'] = r.split(';')
1157
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions} : Properties are: ' + r + '\n')
1158
+ elif count_for_questions == 1:
1159
+ output_all_details += ('~'*100 + '\n')
1160
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1161
+ response = await llm.ainvoke(conversation_history)
1162
+ conversation_history.append(AIMessage(content=response.content))
1163
+ await asyncio.sleep(0)
1164
+ r = response.content
1165
+ five_answers_summary['microstructure'] = r.split(';')
1166
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: Microstructures are: '+ r + '\n')
1167
+ elif count_for_questions == 2:
1168
+ output_all_details += ('~'*100 + '\n')
1169
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1170
+ response = await llm.ainvoke(conversation_history)
1171
+ conversation_history.append(AIMessage(content=response.content))
1172
+ await asyncio.sleep(0)
1173
+ r = response.content
1174
+ composition, key_elements = _parse_composition_and_key_components(r)
1175
+ five_answers_summary['composition&key_elements'] = composition
1176
+ if 'N/A' not in key_elements:
1177
+ five_answers_summary['composition&key_elements'] += '; ' + key_elements
1178
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: ' + r + '\n')
1179
+ elif count_for_questions == 3:
1180
+ output_all_details += ('~'*100 + '\n')
1181
+ prerequisites = f"Prerequisites: Enhanced properties of the alloy: {five_answers_summary['property']}; Microstructure of the alloy: {five_answers_summary['microstructure']}."
1182
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1183
+ response = await llm.ainvoke(conversation_history)
1184
+ conversation_history.append(AIMessage(content=response.content))
1185
+ await asyncio.sleep(0)
1186
+ r = response.content
1187
+ sp = r
1188
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1189
+ elif count_for_questions == 4:
1190
+ output_all_details += ('~'*100 + '\n')
1191
+ conversation_history.append(HumanMessage(content=prompts[count_for_questions]))
1192
+ response = await llm.ainvoke(conversation_history)
1193
+ conversation_history.append(AIMessage(content=response.content))
1194
+ await asyncio.sleep(0)
1195
+ r = response.content
1196
+ five_answers_summary['processing'] = r.split(';')
1197
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}' + ' Processing methods are: ' + r + '\n')
1198
+ elif count_for_questions == 5:
1199
+ output_all_details += ('~'*100 + '\n')
1200
+ prerequisites = f"Processing methods previously discussed: {r}. "
1201
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1202
+ response = await llm.ainvoke(conversation_history)
1203
+ conversation_history.append(AIMessage(content=response.content))
1204
+ await asyncio.sleep(0)
1205
+ r = response.content
1206
+ five_answers_summary['processing'] = r.split('The explanations are:')[0].split(': ')[1]
1207
+ output_all_details += (f'Q{count_for_questions}: ' + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1208
+ elif count_for_questions == 6:
1209
+ output_all_details += ('~'*100 + '\n')
1210
+ prerequisites = f"Prerequisites: Processing methods of the alloy: {five_answers_summary['processing']}; Microstructure of the alloy: {five_answers_summary['microstructure']}. "
1211
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1212
+ response = await llm.ainvoke(conversation_history)
1213
+ conversation_history.append(AIMessage(content=response.content))
1214
+ await asyncio.sleep(0)
1215
+ r = response.content
1216
+ output_all_details += (f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1217
+ elif count_for_questions == 7:
1218
+ output_all_details += ('~'*100 + '\n')
1219
+ prerequisites = f"Prerequisites: Composition & Key components (if any) of the alloy: {five_answers_summary['composition&key_elements']}; Microstructure of the alloy: {five_answers_summary['microstructure']}. "
1220
+ conversation_history.append(HumanMessage(content=prerequisites + prompts[count_for_questions]))
1221
+ response = await llm.ainvoke(conversation_history)
1222
+ conversation_history.append(AIMessage(content=response.content))
1223
+ await asyncio.sleep(0)
1224
+ r = response.content
1225
+ ps = r
1226
+ output_all_details += (f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1227
+ elif count_for_questions == 8:
1228
+ output_all_details += ('~'*100 + '\n')
1229
+ prerequisites = f"Prerequisites: First table for relationsip between processing and microstructure: {ps}; Second table for relationsip between microstructure and properties: {sp}. "
1230
+ conversation_history.append(HumanMessage(content=prerequisites + '\n' + prompts[count_for_questions]))
1231
+ response = await llm.ainvoke(conversation_history)
1232
+ conversation_history.append(AIMessage(content=response.content))
1233
+ await asyncio.sleep(0)
1234
+ r = response.content
1235
+ output_all_details += ('\n' + f'Q{count_for_questions}: ' + prerequisites + prompts[count_for_questions] + '\n' + f'A{count_for_questions}: '+ r + '\n')
1236
+
1237
+ return r, sp, ps, output_all_details
1238
+
1239
+ async def normalize_system_charts(count_for_visualization, prompts, conversation_history, system_chart, raw_chart, paper, llm):
1240
+ """
1241
+ Normalize system charts for visualization
1242
+
1243
+ Args:
1244
+ count_for_visualization: Current visualization step number
1245
+ prompts: List of prompts for normalization
1246
+ conversation_history: Conversation history with LLM
1247
+ system_chart: The extracted system chart content
1248
+ paper: Full content of the paper
1249
+ llm: Language model for processing
1250
+
1251
+ Returns:
1252
+ str: Normalized system chart content
1253
+ """
1254
+ step_descriptions = {
1255
+ 0: "categorizing microstructures",
1256
+ 1: "standardizing properties",
1257
+ 2: "splitting mechanisms",
1258
+ 3: "separating multi-properties",
1259
+ 4: "filling missing values",
1260
+ 5: "enforcing terminology consistency"
1261
+ }
1262
+
1263
+ step_name = step_descriptions.get(count_for_visualization, f"step {count_for_visualization}")
1264
+
1265
+ print(f"\n🔄 NORMALIZATION STEP {count_for_visualization + 1}: {step_name}")
1266
+
1267
+ try:
1268
+
1269
+ # Handle the first step (categorization) with special processing
1270
+ if count_for_visualization == 0:
1271
+ microstructures = _parse_microstructures(system_chart)
1272
+ print(f"🔍 Extracted microstructures: {microstructures}")
1273
+
1274
+ # Question 1: Initial categorization
1275
+ conversation_history.append(HumanMessage(content=prompts[count_for_visualization] + microstructures + '\nOriginal paper: ' + paper))
1276
+ response = await llm.ainvoke(conversation_history)
1277
+ conversation_history.append(AIMessage(content=response.content))
1278
+ result = response.content
1279
+ elif count_for_visualization == 1:
1280
+ conversation_history.append(HumanMessage(content=prompts[count_for_visualization] + raw_chart))
1281
+ response = await llm.ainvoke(conversation_history)
1282
+ conversation_history.append(AIMessage(content=response.content))
1283
+ result = response.content
1284
+ else:
1285
+ conversation_history.append(HumanMessage(content=prompts[count_for_visualization]))
1286
+ response = await llm.ainvoke(conversation_history)
1287
+ conversation_history.append(AIMessage(content=response.content))
1288
+ result = response.content
1289
+
1290
+ return result
1291
+
1292
+ except Exception as e:
1293
+ print(f"❌ Error in normalization step {count_for_visualization}: {str(e)}")
1294
+ print(f"📤 Returning original chart due to error\n")
1295
+ return system_chart # Return original chart if normalization fails
1296
+
1297
+ async def create_visualization(normalized_chart, paper_title, temp_dir, created_visualizations):
1298
+ """
1299
+ Create visualization from normalized system chart
1300
+
1301
+ Args:
1302
+ normalized_chart: The normalized system chart content
1303
+ paper_title: Title of the paper
1304
+ temp_dir: Temporary directory to save visualization files
1305
+ created_visualizations: List to store created visualization file paths
1306
+
1307
+ Returns:
1308
+ str: Path to the created visualization file
1309
+ """
1310
+ try:
1311
+ # Create the network diagram with the normalized result
1312
+ visualization_path = create_network_diagram(normalized_chart, paper_title, temp_dir)
1313
+
1314
+ # Add to the list of created visualizations
1315
+ created_visualizations.append(visualization_path)
1316
+
1317
+ print(f"✅ Visualization created for '{paper_title}': {visualization_path}")
1318
+ return visualization_path
1319
+
1320
+ except Exception as e:
1321
+ print(f"❌ Error creating visualization for '{paper_title}': {str(e)}")
1322
+ return None
system_chart_service.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+ import os
3
+ import shutil
4
+ from io import BytesIO
5
+ from zipfile import ZipFile
6
+ from typing import Dict, Callable, Any, List
7
+ from datetime import datetime
8
+ from system_chart_extraction import system_chart_extraction
9
+
10
+ class SystemChartService:
11
+ """
12
+ Service class for System Chart Extraction.
13
+ Handles the orchestration of system chart extraction, visualization creation,
14
+ and result packaging.
15
+ """
16
+
17
+ def __init__(self):
18
+ pass
19
+
20
+ async def extract_system_charts(
21
+ self,
22
+ category: str,
23
+ llm,
24
+ pdf_dict: Dict[str, str],
25
+ enable_visualization: bool = False,
26
+ run_system_chart_normalization: bool = False,
27
+ progress_callback: Callable[[Dict[str, Any]], None] = None,
28
+ extraction_id: str = None
29
+ ) -> Dict[str, Any]:
30
+ """
31
+ Main system chart extraction pipeline.
32
+
33
+ Args:
34
+ category: Database category for extraction
35
+ llm: Language model for extraction
36
+ pdf_dict: Dictionary mapping document titles to full text content
37
+ enable_visualization: Whether to create visualization diagrams
38
+ progress_callback: Function to report progress updates
39
+
40
+ Returns:
41
+ Dict containing extraction results and metadata
42
+ """
43
+ try:
44
+ # Validate input
45
+ if not pdf_dict:
46
+ return {
47
+ "success": False,
48
+ "message": "No files found in the specified category.",
49
+ "zip_data": None,
50
+ "filename": None
51
+ }
52
+
53
+ print(f"📄 Found {len(pdf_dict)} papers to process")
54
+
55
+ # Initial progress update
56
+ if progress_callback:
57
+ progress_callback({
58
+ "status": "starting",
59
+ "progress": 0,
60
+ "message": "Getting PDF files..."
61
+ })
62
+
63
+ # Create progress callback function for the extraction process
64
+ def extraction_progress_callback(progress_data):
65
+ if progress_callback:
66
+ print(f"📊 Progress update: {progress_data}")
67
+ progress_callback(progress_data)
68
+ print(f"🔄 Progress updated: {progress_data}")
69
+
70
+ # Update progress before starting extraction
71
+ if progress_callback:
72
+ progress_callback({
73
+ "status": "starting",
74
+ "progress": 5,
75
+ "message": f"Found {len(pdf_dict)} papers. Starting extraction..."
76
+ })
77
+
78
+ print(f"🔬 Starting system chart extraction...")
79
+
80
+ # Create temporary directory for visualization files if needed
81
+ temp_dir = None
82
+ if enable_visualization:
83
+ temp_dir = tempfile.mkdtemp(prefix="system_chart_viz_")
84
+ print(f"📁 Created temporary directory: {temp_dir}")
85
+
86
+ # Run system chart extraction with progress tracking
87
+ output_system_charts, output_all_details, created_visualizations = await system_chart_extraction(
88
+ pdf_dict,
89
+ llm,
90
+ extraction_progress_callback,
91
+ enable_visualization,
92
+ temp_dir,
93
+ extraction_id, # Pass extraction_id to save conversations
94
+ run_normalization=run_system_chart_normalization # ← only normalize when asked/needed
95
+ )
96
+
97
+ # Create ZIP file in memory
98
+ zip_data = self._create_zip_file(
99
+ output_system_charts,
100
+ output_all_details,
101
+ created_visualizations
102
+ )
103
+
104
+ # Clean up temporary directory
105
+ if temp_dir:
106
+ self._cleanup_temp_directory(temp_dir)
107
+
108
+ # Prepare result
109
+ viz_message = f" with {len(created_visualizations)} visualizations" if created_visualizations else ""
110
+
111
+ return {
112
+ "success": True,
113
+ "message": f"System chart extraction completed successfully{viz_message}!",
114
+ "zip_data": zip_data,
115
+ "filename": "system_chart_extraction.zip",
116
+ "charts_count": len(pdf_dict),
117
+ "visualizations_count": len(created_visualizations) if created_visualizations else 0
118
+ }
119
+
120
+ except Exception as e:
121
+ # Clean up on error
122
+ if 'temp_dir' in locals() and temp_dir:
123
+ self._cleanup_temp_directory(temp_dir)
124
+
125
+ return {
126
+ "success": False,
127
+ "message": f"Error during system chart extraction: {str(e)}",
128
+ "zip_data": None,
129
+ "filename": None
130
+ }
131
+
132
+ def _create_zip_file(
133
+ self,
134
+ output_system_charts: str,
135
+ output_all_details: str,
136
+ created_visualizations: List[str]
137
+ ) -> bytes:
138
+ """Create ZIP file containing extraction results and visualizations."""
139
+ zip_buffer = BytesIO()
140
+
141
+ with ZipFile(zip_buffer, "w") as zipf:
142
+ # Add text results
143
+ zipf.writestr("system_charts.txt", output_system_charts)
144
+ zipf.writestr("all_details.txt", output_all_details)
145
+
146
+ # Add visualization files if any were created
147
+ if created_visualizations:
148
+ for viz_path in created_visualizations:
149
+ if os.path.exists(viz_path):
150
+ # Extract just the filename for the ZIP entry
151
+ viz_filename = os.path.basename(viz_path)
152
+ zipf.write(viz_path, f"visualizations/{viz_filename}")
153
+ print(f"📦 Added visualization to ZIP: {viz_filename}")
154
+
155
+ # Get ZIP data and close buffer
156
+ zip_data = zip_buffer.getvalue()
157
+ zip_buffer.close()
158
+
159
+ return zip_data
160
+
161
+ def _cleanup_temp_directory(self, temp_dir: str) -> None:
162
+ """Clean up temporary directory and its contents."""
163
+ if temp_dir and os.path.exists(temp_dir):
164
+ try:
165
+ shutil.rmtree(temp_dir)
166
+ print(f"🗑️ Cleaned up temporary directory: {temp_dir}")
167
+ except OSError as e:
168
+ print(f"⚠️ Could not clean up temporary directory {temp_dir}: {e}")
169
+ # Don't fail the whole process if cleanup fails
170
+ pass
171
+
172
+ @staticmethod
173
+ def parse_system_charts_file(file_content: str) -> List[Dict[str, str]]:
174
+ """
175
+ Parse a delimited system charts file into individual chart objects.
176
+
177
+ Args:
178
+ file_content: Content of the system_charts.txt file
179
+
180
+ Returns:
181
+ List of dictionaries, each containing:
182
+ - 'chart_number': The chart number (auto-generated)
183
+ - 'title': Paper title
184
+ - 'citation': Citation format (same as title for compatibility)
185
+ - 'content': System chart table content
186
+ """
187
+ charts = []
188
+ lines = file_content.split('\n')
189
+
190
+ current_chart = None
191
+ chart_number = 1
192
+
193
+ for i, line in enumerate(lines):
194
+ line = line.strip()
195
+
196
+ # Check for delimiter line (100 '=' characters)
197
+ if line == '=' * 100:
198
+ # Look for PAPER: line after delimiter
199
+ if i + 1 < len(lines):
200
+ next_line = lines[i + 1].strip()
201
+ if next_line.startswith('PAPER:'):
202
+ # Save previous chart if exists
203
+ if current_chart:
204
+ charts.append(current_chart)
205
+
206
+ # Start new chart
207
+ paper_title = next_line.replace('PAPER:', '').strip()
208
+ current_chart = {
209
+ 'chart_number': str(chart_number),
210
+ 'title': paper_title,
211
+ 'citation': f'[{paper_title}]',
212
+ 'content': ''
213
+ }
214
+ chart_number += 1
215
+
216
+ elif line.startswith('SYSTEM_CHART:') and current_chart:
217
+ # Extract the system chart content
218
+ chart_content = line.replace('SYSTEM_CHART:', '').strip()
219
+ current_chart['content'] = chart_content
220
+
221
+ # Handle last chart if file doesn't end with proper delimiter
222
+ if current_chart:
223
+ charts.append(current_chart)
224
+
225
+ print(f"📊 Parsed {len(charts)} system charts from file")
226
+ return charts
227
+
228
+ @staticmethod
229
+ def combine_system_charts_files(file_contents: List[str]) -> str:
230
+ """
231
+ Combine multiple system chart txt files into a single formatted string.
232
+ Preserves the one-to-one relationship between system charts and titles.
233
+
234
+ Args:
235
+ file_contents: List of system chart file contents to combine
236
+
237
+ Returns:
238
+ Combined system chart content in the new format
239
+ """
240
+ combined_charts = []
241
+
242
+ # Parse each file and collect all charts
243
+ for file_content in file_contents:
244
+ charts = SystemChartService.parse_system_charts_file(file_content)
245
+ combined_charts.extend(charts)
246
+
247
+ # Generate combined output in new format
248
+ combined_output = "COMBINED SYSTEM CHARTS EXTRACTION RESULTS\n"
249
+ combined_output += "=" * 100 + "\n"
250
+
251
+ for chart in combined_charts:
252
+ delimiter = "=" * 100
253
+ combined_output += f'\n{delimiter}\n'
254
+ combined_output += f'PAPER: {chart["title"]}\n'
255
+ combined_output += f'SYSTEM_CHART: {chart["content"]}\n'
256
+ combined_output += f'{delimiter}\n\n'
257
+
258
+ # Add summary
259
+ summary_delimiter = "=" * 100
260
+ combined_output += f'\n{summary_delimiter}\n'
261
+ combined_output += f'COMBINATION SUMMARY\n'
262
+ combined_output += f'{summary_delimiter}\n'
263
+ combined_output += f'Total Files Combined: {len(file_contents)}\n'
264
+ combined_output += f'Total System Charts: {len(combined_charts)}\n'
265
+ combined_output += f'Combination Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n'
266
+
267
+ print(f"🔗 Combined {len(file_contents)} files into {len(combined_charts)} system charts")
268
+ return combined_output
269
+
270
+ async def combine_multiple_system_chart_files(
271
+ self,
272
+ file_paths: List[str],
273
+ output_filename: str = "combined_system_charts.txt"
274
+ ) -> Dict[str, Any]:
275
+ """
276
+ Combine multiple system chart txt files into a single ZIP file.
277
+
278
+ Args:
279
+ file_paths: List of file paths to system chart txt files
280
+ output_filename: Name for the output file
281
+
282
+ Returns:
283
+ Dict containing success status, message, and ZIP data
284
+ """
285
+ try:
286
+ file_contents = []
287
+
288
+ # Read all files
289
+ for file_path in file_paths:
290
+ if not os.path.exists(file_path):
291
+ return {
292
+ "success": False,
293
+ "message": f"File not found: {file_path}",
294
+ "zip_data": None,
295
+ "filename": None
296
+ }
297
+
298
+ with open(file_path, 'r', encoding='utf-8') as f:
299
+ file_contents.append(f.read())
300
+
301
+ # Combine the files
302
+ combined_content = self.combine_system_charts_files(file_contents)
303
+
304
+ # Create ZIP file
305
+ zip_buffer = BytesIO()
306
+ with ZipFile(zip_buffer, "w") as zipf:
307
+ zipf.writestr(output_filename, combined_content)
308
+
309
+ zip_data = zip_buffer.getvalue()
310
+ zip_buffer.close()
311
+
312
+ return {
313
+ "success": True,
314
+ "message": f"Successfully combined {len(file_paths)} system chart files!",
315
+ "zip_data": zip_data,
316
+ "filename": "combined_system_charts.zip",
317
+ "files_combined": len(file_paths)
318
+ }
319
+
320
+ except Exception as e:
321
+ return {
322
+ "success": False,
323
+ "message": f"Error combining system chart files: {str(e)}",
324
+ "zip_data": None,
325
+ "filename": None
326
+ }
327
+
328
+ @staticmethod
329
+ def get_chart_citations(file_content: str) -> List[str]:
330
+ """
331
+ Extract just the citations from a system charts file.
332
+
333
+ Args:
334
+ file_content: Content of the system_charts.txt file
335
+
336
+ Returns:
337
+ List of citation strings
338
+ """
339
+ charts = SystemChartService.parse_system_charts_file(file_content)
340
+ return [chart.get('citation', chart.get('title', f"Chart {chart['chart_number']}"))
341
+ for chart in charts if chart.get('chart_number')]
vectorstore.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import hashlib
4
+ from langchain_community.vectorstores import Chroma
5
+ from langchain_openai import OpenAIEmbeddings
6
+
7
+ def get_base_directory():
8
+ """Return the base directory for persistent storage (next to executable when packaged)"""
9
+ if hasattr(sys, '_MEIPASS'): # Running as PyInstaller executable
10
+ return os.path.dirname(sys.executable)
11
+ else: # Running as normal Python script
12
+ return os.path.dirname(os.path.abspath(__file__))
13
+
14
+ # Define path relative to the base directory
15
+ CHROMA_DB_BASE = os.path.join(get_base_directory(), "root databases")
16
+ os.makedirs(CHROMA_DB_BASE, exist_ok=True)
17
+
18
+ def compute_file_hash(content: bytes) -> str:
19
+ return hashlib.md5(content).hexdigest()
20
+
21
+ def get_vectorstore(category: str, embedding_model):
22
+ path = os.path.join(CHROMA_DB_BASE, category.strip())
23
+ os.makedirs(path, exist_ok=True)
24
+ return Chroma(persist_directory=path, embedding_function=embedding_model)