import regex as re from typing import Dict, List, Tuple from langchain_core.messages import HumanMessage from prompts import create_hypotheses_generation_prompt from typing import Tuple def _content_to_text(content) -> str: """ Normalize LLM content payloads (string/list/dict) into plain text. Anthropic and Gemini models may return a list of content blocks instead of a plain string (same normalization as llm_content_to_text in main.py). """ if content is None: return "" if isinstance(content, str): return content if isinstance(content, list): parts = [] for item in content: text = _content_to_text(item) if text: parts.append(text) return "\n".join(parts) if isinstance(content, dict): for key in ("text", "content", "output_text"): if key in content: return _content_to_text(content.get(key)) return str(content) return str(content) PATTERN = re.compile( r"PAPER:\s*(?P.*?)\s*[\r\n]+SYSTEM_CHART:\s*(?P.*?)(?=\s*={5,}|$)", re.DOTALL ) HYPOTHESIS_HEADING = re.compile(r"^(?P[ \t#*]*Hypothesis\s*#?\s*)\d+", re.MULTILINE | re.IGNORECASE) def number_hypotheses(iterations: List[Dict]) -> Tuple[List[str], List[Dict]]: """ Renumber "Hypothesis #k" headings continuously across iterations. Returns the renumbered iteration texts and a flat list of {number, iteration, text}. An iteration with no parseable heading becomes a single hypothesis. """ counter = 0 def bump(match): nonlocal counter counter += 1 return f"{match.group('lead')}{counter}" texts, hypotheses = [], [] for it in iterations: text = HYPOTHESIS_HEADING.sub(bump, it['content']) starts = [m.start() for m in HYPOTHESIS_HEADING.finditer(text)] if not starts: counter += 1 text = f"Hypothesis #{counter}:\n{text.strip()}" starts = [0] texts.append(text) first = counter - len(starts) + 1 for i, (start, end) in enumerate(zip(starts, starts[1:] + [len(text)])): hypotheses.append({'number': first + i, 'iteration': it['iteration'], 'text': text[start:end].strip()}) return texts, hypotheses def parse_multiple_system_chart_files(content_list: List[str]) -> Tuple[str, str]: """ Parse multiple system chart files and extract numbered tables and citations. Handles new format with 100 '=' delimiters and PAPER:/SYSTEM_CHART: structure. Args: content_list: List of raw content strings from multiple system chart files Returns: Tuple of (numbered_tables, numbered_citations) """ papers: List[str] = [] charts: List[str] = [] for blob in content_list: if not blob or not blob.strip(): continue for m in PATTERN.finditer(blob): papers.append(m.group("paper").strip()) charts.append(m.group("chart").strip()) numbered_tables = [] numbered_citations = [] for i, (p, c) in enumerate(zip(papers, charts), 1): numbered_tables.append(f"\n=== TABLE {i} ===\n{c}\n=== END OF TABLE {i} ===") numbered_citations.append(f"{i}. {p}") return "\n".join(numbered_tables).strip(), "\n".join(numbered_citations).strip() def parse_system_charts_and_citations(content: str) -> Tuple[str, str]: """ Parse system charts content and extract numbered tables and citations robustly. Updated to handle new format with 100 '=' delimiters and PAPER:/SYSTEM_CHART: structure. Args: content: Raw content from system_charts.txt file Returns: Tuple of (numbered_tables, numbered_citations) """ # Use the new multi-file parser with single content return parse_multiple_system_chart_files([content]) async def generate_hypotheses_from_system_charts( system_charts: List[Dict[str, str]], llm, material_goal_1: str = "Property #1 for xxx material", material_goal_2: str = "Property #2 for xxx material", iterations_count: int = 1, progress_callback=None ) -> Tuple[str, List[Dict]]: """ Generate scientific hypotheses using the new synergistic approach. Args: system_charts: List of parsed system chart dictionaries with 'content' or 'raw_content' fields llm: Language model for hypothesis generation material_goal_1: First material design goal to guide hypothesis generation material_goal_2: Second material design goal to guide hypothesis generation iterations_count: Number of hypothesis iterations to generate progress_callback: Function to report progress updates Returns: (hypotheses_content, hypotheses) where hypotheses is a list of {number, iteration, text} numbered continuously across iterations """ if not system_charts: return "No system charts provided for hypothesis generation.", [] # Initial progress update if progress_callback: progress_callback({ "status": "parsing", "progress": 5, "message": "Parsing system charts and citations..." }) # Extract raw content from system charts # Support both 'raw_content' and 'content' fields for backward compatibility content_list = [] for chart in system_charts: raw_content = chart.get('raw_content', chart.get('content', '')) if raw_content and raw_content.strip(): content_list.append(raw_content) if not content_list: return "No valid system chart content found for hypothesis generation.", [] # Parse all system chart files together using the new multi-file parser numbered_tables, numbered_citations = parse_multiple_system_chart_files(content_list) if progress_callback: progress_callback({ "status": "generating", "progress": 20, "message": "Generating synergistic hypotheses..." }) # Create the prompt with user-specified material goals hypotheses_prompt = create_hypotheses_generation_prompt(material_goal_1, material_goal_2) # Prepare the prompt in the specified format full_prompt = f"Tables are: {numbered_tables}\n\nCitation resources are: {numbered_citations}\n\n{hypotheses_prompt}" # Configure LLM parameters for creativity llm_kwargs = {} # Check if this is a reasoning model (o1 series) which doesn't support temperature model_name = getattr(llm, 'model_name', getattr(llm, 'model', '')) if not (model_name and model_name.lower().startswith('o')): # Set higher temperature for creativity (non-reasoning models) if hasattr(llm, 'temperature'): original_temp = llm.temperature llm.temperature = 1.0 # Higher temperature for creativity elif hasattr(llm, 'model_kwargs'): llm_kwargs = llm.model_kwargs.copy() llm_kwargs['temperature'] = 1.0 try: # Generate hypotheses for each iteration all_iterations_content = [] for iteration in range(iterations_count): # Update progress for current iteration if progress_callback: progress_start = 20 + (iteration * 60 // iterations_count) progress_callback({ "status": "llm_processing", "progress": progress_start, "message": f"LLM generating hypotheses (iteration {iteration + 1}/{iterations_count})..." }) conversation_history = [HumanMessage(content=full_prompt)] # Generate with appropriate parameters if llm_kwargs: # Use model_kwargs if available original_kwargs = getattr(llm, 'model_kwargs', {}) llm.model_kwargs = llm_kwargs response = await llm.ainvoke(conversation_history) llm.model_kwargs = original_kwargs else: response = await llm.ainvoke(conversation_history) iteration_content = _content_to_text(response.content) all_iterations_content.append({ 'iteration': iteration + 1, 'content': iteration_content }) if progress_callback: progress_callback({ "status": "formatting", "progress": 80, "message": "Formatting results..." }) # Number hypotheses continuously across iterations, then format renumbered_texts, hypotheses = number_hypotheses(all_iterations_content) iterations_output = "" for iteration_data, text in zip(all_iterations_content, renumbered_texts): iterations_output += f"\n\n" iterations_output += "=" * 80 + "\n" iterations_output += f"ITERATION {iteration_data['iteration']} HYPOTHESES:\n" iterations_output += "=" * 80 + "\n\n" iterations_output += text + "\n\n" iterations_output += "=" * 80 + "\n" # Create formatted output formatted_output = "" formatted_output += "NUMBERED SYSTEM CHARTS:\n" formatted_output += numbered_tables + "\n\n" formatted_output += "CITATION REFERENCES:\n" formatted_output += numbered_citations formatted_output += iterations_output # Restore original temperature if modified if hasattr(llm, 'temperature') and 'original_temp' in locals(): llm.temperature = original_temp if progress_callback: progress_callback({ "status": "completed", "progress": 100, "message": f"Generated {iterations_count} iteration{'s' if iterations_count > 1 else ''} of synergistic hypotheses from {len(system_charts)} charts" }) return formatted_output, hypotheses except Exception as e: # Restore original temperature if modified if hasattr(llm, 'temperature') and 'original_temp' in locals(): llm.temperature = original_temp error_msg = f"Error generating hypotheses: {str(e)}" return error_msg, []