docs/Agent/experiment_handler_patch.py
2025-05-07 14:28:10 +00:00

348 lines
14 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Code snippet to be added to agent_runner.py to handle creating experiments with multiblock markdown templates.
This would extend the existing functionality to support the richer experiment format.
"""
def handle_create_multiblock_experiment(self, args: Dict[str, Any]):
"""
Handle creation of a new experiment using the multiblock markdown template.
This creates a structured markdown file with multiple YAML frontmatter blocks
and placeholder sections for data, analysis, and next steps.
Args should include:
title (str): Experiment title
researchers (list): List of researchers involved
protocol_id (str): Protocol ID (e.g., PROT-XXXX)
protocol_name (str): Protocol name
project (str): Project name
aim (str): Brief description of experimental aim
cell_lines (list): List of cell lines used
plate_format (str): Plate format (e.g., 24-well)
condition_map (str): Map of conditions in the plate
additional_metadata (dict): Any additional metadata fields
"""
# Load the multiblock template
template_path = os.path.join("Templates", "experiment_multiblock.md")
with open(template_path, "r") as f:
template = f.read()
# Generate experiment ID if not provided
if not args.get("experiment_id"):
# Generate unique ID with date-based prefix
date_prefix = datetime.now().strftime("%Y%m%d")
existing_ids = [f for f in os.listdir("Experiments") if f.startswith("EXP-")]
existing_nums = [int(f.split("-")[1].split("_")[0])
for f in existing_ids if re.match(r"EXP-\d+", f)]
next_num = max(existing_nums) + 1 if existing_nums else 1
args["experiment_id"] = f"EXP-{next_num:04d}"
# Current date if not provided
if not args.get("date"):
args["date"] = datetime.now().strftime("%Y-%m-%d")
# Fill template with args
# This is a simple placeholder - in a real implementation, we'd handle the
# multiple frontmatter blocks more carefully
for key, value in args.items():
if isinstance(value, (str, int, float)):
template = template.replace(f"{{{{{key}}}}}", str(value))
# Generate filename with experiment ID and title
experiment_id = args.get("experiment_id")
title = args.get("title", "untitled").lower().replace(" ", "-")
filename = f"{experiment_id}-{title}.md"
out_path = os.path.join("Experiments", filename)
# Ensure unique filename if exists
i = 1
base_filename = filename
while os.path.exists(out_path):
filename = f"{experiment_id}-{title}-{i}.md"
out_path = os.path.join("Experiments", filename)
i += 1
# Create data directories for the experiment
data_dir = os.path.join("Data", experiment_id)
os.makedirs(os.path.join(data_dir, "raw"), exist_ok=True)
os.makedirs(os.path.join(data_dir, "figures"), exist_ok=True)
# Create analysis script placeholder if needed
analysis_dir = "Analysis"
os.makedirs(analysis_dir, exist_ok=True)
# Script path based on experiment type
if args.get("analysis_type") == "mRNA_stability":
script_path = os.path.join(analysis_dir, f"{experiment_id}_mRNA_stability_analysis.R")
# Here we would create a placeholder R script tailored to mRNA stability analysis
elif args.get("analysis_type") == "qPCR":
script_path = os.path.join(analysis_dir, f"{experiment_id}_qPCR_analysis.R")
# Create a placeholder qPCR analysis script
# Write the experiment file
with open(out_path, "w") as f:
f.write(template)
# Log the action
self.logger.info(f"Created multiblock experiment: {out_path}")
self.logger.info(f"Created data directories: {data_dir}/raw and {data_dir}/figures")
# Update user profile
for researcher in args.get("researchers", []):
researcher_id = researcher.replace(" ", "_").lower()
self._update_user_profile(researcher_id, "recent_experiments", experiment_id)
for cell_line in args.get("cell_lines", []):
if isinstance(cell_line, dict) and "name" in cell_line:
self._update_user_profile(researcher_id, "frequent_cell_lines", cell_line["name"])
elif isinstance(cell_line, str):
self._update_user_profile(researcher_id, "frequent_cell_lines", cell_line)
# Append to CHANGELOG.md
self.append_changelog(f"Created new multiblock experiment {experiment_id}: {args.get('title')}")
# Check if there are any experiment tasks to add to TASKS.md
if args.get("next_steps"):
self.add_experiment_tasks_to_tasklist(experiment_id, args.get("next_steps"))
return {
"experiment_id": experiment_id,
"path": out_path,
"data_dir": data_dir,
"analysis_script": script_path if "script_path" in locals() else None
}
def handle_update_multiblock_experiment(self, args: Dict[str, Any]):
"""
Handle updating an existing multiblock experiment markdown file.
Args should include:
experiment_id (str): Experiment ID to update
section (str): Section to update (metadata, sample_metadata, results, interpretation, etc.)
content (dict or str): Content to update in the section
next_steps (list, optional): Updated next steps list
status (str, optional): New experiment status
"""
experiment_id = args.get("experiment_id")
if not experiment_id:
self.logger.error("Missing experiment_id for update_multiblock_experiment.")
return
# Find experiment file
exp_dir = "Experiments"
exp_file = None
for fname in os.listdir(exp_dir):
if experiment_id in fname and fname.endswith(".md"):
exp_file = os.path.join(exp_dir, fname)
break
if not exp_file or not os.path.exists(exp_file):
self.logger.error(f"Experiment file not found for id: {experiment_id}")
return
# Read the current file content
with open(exp_file, "r") as f:
content = f.read()
# Process updates - this is a simplified example
# A real implementation would parse the Markdown and YAML blocks properly
section = args.get("section")
section_content = args.get("content")
# Handle status updates
if args.get("status"):
new_status = args.get("status")
# Update status in the YAML frontmatter
status_pattern = r"status: .*"
content = re.sub(status_pattern, f"status: {new_status}", content)
# Handle next steps updates
if args.get("next_steps"):
# Locate the Next Steps section and replace it
next_steps_pattern = r"# 5⃣ Next Steps ✅.*?(?=# 6⃣|$)"
next_steps_content = "# 5⃣ Next Steps ✅\n_Check boxes when complete. These can auto-update TASKS.md._\n\n"
for step in args.get("next_steps"):
checked = "x" if step.get("completed") else " "
next_steps_content += f"- [{checked}] {step.get('description')}\n"
content = re.sub(next_steps_pattern, next_steps_content, content, flags=re.DOTALL)
# Update TASKS.md based on checked items
self.update_tasks_from_experiment(experiment_id, args.get("next_steps"))
# Handle section-specific updates
if section and section_content:
if section.lower() in ["metadata", "sample_metadata", "reagents"]:
# Update YAML frontmatter blocks
# This would require more sophisticated YAML parsing in a real implementation
pass
elif section.lower() in ["results", "interpretation", "discussion"]:
# Update markdown sections
section_pattern = rf"## {section.title()}.*?(?=##|$)"
new_section = f"## {section.title()}\n{section_content}\n\n"
content = re.sub(section_pattern, new_section, content, flags=re.DOTALL)
# Write updated content back to file
with open(exp_file, "w") as f:
f.write(content)
# Log the update
self.logger.info(f"Updated multiblock experiment: {exp_file}")
# Append to CHANGELOG.md
self.append_changelog(f"Updated experiment {experiment_id}: {section if section else 'various sections'}")
# If experiment is completed, verify all required fields are present
if args.get("status") == "completed":
self.validate_experiment_completion(experiment_id, exp_file)
return {
"experiment_id": experiment_id,
"path": exp_file,
"updated_section": section
}
def validate_experiment_completion(self, experiment_id, file_path):
"""Validate that a completed experiment has all required fields."""
with open(file_path, "r") as f:
content = f.read()
required_sections = [
"# 3⃣ Results & Analysis",
"# 4⃣ Interpretation"
]
missing = []
for section in required_sections:
if section not in content or re.search(rf"{section}.*?_[^_]*_", content, re.DOTALL):
# Section missing or only contains placeholder text
missing.append(section.replace("#", "").strip())
if missing:
issue_title = f"Experiment {experiment_id} missing required sections"
issue_body = f"The following required sections need to be completed: {', '.join(missing)}. Please update the experiment record."
self.handle_open_issue({"title": issue_title, "body": issue_body})
return False
# Mark related tasks as complete in TASKS.md
self.mark_experiment_complete_in_tasks(experiment_id)
return True
def update_tasks_from_experiment(self, experiment_id, next_steps):
"""Update TASKS.md based on experiment next steps."""
if not os.path.exists("TASKS.md"):
return
with open("TASKS.md", "r") as f:
tasks_content = f.readlines()
# Find experiment section in TASKS.md or create it
exp_section_idx = -1
for i, line in enumerate(tasks_content):
if experiment_id in line and "##" in line:
exp_section_idx = i
break
if exp_section_idx == -1:
# Section not found, append at the end of Lab Tasks
lab_tasks_idx = -1
for i, line in enumerate(tasks_content):
if "## Lab Tasks" in line:
lab_tasks_idx = i
break
if lab_tasks_idx != -1:
# Create new section
tasks_content.insert(lab_tasks_idx + 1, f"### {experiment_id} Tasks\n")
exp_section_idx = lab_tasks_idx + 1
else:
# Create Lab Tasks section and experiment section
tasks_content.append("\n## Lab Tasks\n")
tasks_content.append(f"### {experiment_id} Tasks\n")
exp_section_idx = len(tasks_content) - 1
# Update or add tasks under this section
updated_tasks = []
for step in next_steps:
checked = "x" if step.get("completed") else " "
updated_tasks.append(f"- [{checked}] {step.get('description')}\n")
# Find the end of the section
end_idx = len(tasks_content)
for i in range(exp_section_idx + 1, len(tasks_content)):
if tasks_content[i].startswith("##"):
end_idx = i
break
# Replace the tasks in this section
new_content = tasks_content[:exp_section_idx + 1] + updated_tasks + tasks_content[end_idx:]
with open("TASKS.md", "w") as f:
f.writelines(new_content)
self.logger.info(f"Updated {experiment_id} tasks in TASKS.md")
def mark_experiment_complete_in_tasks(self, experiment_id):
"""Mark all tasks for an experiment as complete in TASKS.md when the experiment is completed."""
if not os.path.exists("TASKS.md"):
return
with open("TASKS.md", "r") as f:
tasks_content = f.readlines()
updated = False
in_experiment_section = False
for i, line in enumerate(tasks_content):
if experiment_id in line and "##" in line:
in_experiment_section = True
continue
if in_experiment_section:
if line.startswith("##"):
# End of section
in_experiment_section = False
elif line.strip().startswith("- [ ]"):
# Unchecked task in this experiment, mark as done
tasks_content[i] = line.replace("- [ ]", "- [x]", 1)
updated = True
if updated:
with open("TASKS.md", "w") as f:
f.writelines(tasks_content)
self.logger.info(f"Marked all tasks for {experiment_id} as complete in TASKS.md")
def add_experiment_tasks_to_tasklist(self, experiment_id, tasks):
"""Add tasks from experiment next steps to TASKS.md."""
if not os.path.exists("TASKS.md"):
with open("TASKS.md", "w") as f:
f.write("# Lab Task List\n\n## Lab Tasks\n")
with open("TASKS.md", "r") as f:
content = f.read()
# Check if Lab Tasks section exists
if "## Lab Tasks" not in content:
content += "\n## Lab Tasks\n"
# Check if this experiment already has a section
if f"### {experiment_id}" in content:
# Will be handled by update_tasks_from_experiment
return
# Add new section for this experiment
new_section = f"\n### {experiment_id} Tasks\n"
for task in tasks:
new_section += f"- [ ] {task.get('description')}\n"
# Insert after Lab Tasks header
lab_tasks_idx = content.find("## Lab Tasks")
if lab_tasks_idx != -1:
insert_idx = lab_tasks_idx + len("## Lab Tasks") + 1
content = content[:insert_idx] + new_section + content[insert_idx:]
else:
content += new_section
with open("TASKS.md", "w") as f:
f.write(content)
self.logger.info(f"Added {experiment_id} tasks to TASKS.md")