Spaces:
Configuration error
Configuration error
Commit ·
ceaf3bb
0
Parent(s):
Initial commit - chatbot online version
Browse files- Dockerfile +12 -0
- conversation_storage.py +78 -0
- hypotheses_generation.py +207 -0
- hypotheses_service.py +279 -0
- index.html +0 -0
- main.py +0 -0
- pdf_text_processing.py +229 -0
- prompts.py +230 -0
- requirements.txt +18 -0
- system_chart_extraction.py +1322 -0
- system_chart_service.py +341 -0
- vectorstore.py +24 -0
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)
|