""" File-based human feedback reader for human feedback during discovery process. The human edits a markdown file via the dashboard or any text editor. The discovery loop reads it each iteration -- if it has content, that content is appended to (or replaces) the LLM system message. """ import logging import os import time as _time logger = logging.getLogger(__name__) _INITIAL_TEMPLATE = """\ # Human Feedback for SkyDiscover # Edit this file to guide the discovery process. # Your text will be APPENDED to the LLM system message at the next iteration. # Toggle between Append and Replace mode in the dashboard. # Clear this file (or delete all non-comment lines) to revert to the default. # Lines starting with # are ignored. # # Examples: # Focus on hexagonal packing and computational geometry approaches. # Use numpy vectorization, avoid loops. Prioritize cache-friendly access patterns. """ MAX_FEEDBACK_CHARS = 4000 class HumanFeedbackReader: """ Reads human feedback from a markdown file on disk. The dashboard writes via write_from_dashboard(); the discovery loop reads via read(). External editors can also modify the file directly. Supports two modes: - "append" (default): feedback is appended to the system message - "replace": feedback replaces the system message entirely """ def __init__(self, feedback_file_path: str, mode: str = "append"): self.path = os.path.abspath(feedback_file_path) self.mode = mode if mode in ("append", "replace") else "append" self._last_content: str = "" self._current_system_prompt: str = "" self._history: list = [] self._create_initial_file() def _create_initial_file(self) -> None: """Create the feedback file with instructions if it doesn't exist.""" if not os.path.exists(self.path): os.makedirs(os.path.dirname(self.path), exist_ok=True) with open(self.path, "w") as f: f.write(_INITIAL_TEMPLATE) logger.info(f"Created human feedback file: {self.path}") def read(self) -> str: """ Read current feedback, stripping comment lines. Returns empty string if file is empty, missing, or only has comments. """ try: with open(self.path, "r") as f: raw = f.read() except (FileNotFoundError, PermissionError): return "" lines = [] for line in raw.splitlines(): stripped = line.strip() if stripped and not stripped.startswith("#"): lines.append(line) content = "\n".join(lines).strip() if len(content) > MAX_FEEDBACK_CHARS: content = content[:MAX_FEEDBACK_CHARS] if content != self._last_content: if content: logger.info(f"Human feedback updated ({len(content)} chars)") elif self._last_content: logger.info("Human feedback cleared") self._last_content = content return content def write_from_dashboard(self, text: str) -> None: """ Write feedback from the dashboard UI. Pass empty string to clear feedback. """ self._write_feedback(text) def set_mode(self, mode: str) -> None: """Set feedback mode: 'append' or 'replace'.""" if mode not in ("append", "replace"): logger.warning(f"Invalid human feedback mode '{mode}', ignoring") return self.mode = mode logger.info(f"Human feedback mode set to: {mode}") def apply_feedback(self, prompt: dict) -> dict: """Apply current feedback to a prompt dict. In append mode, feedback is added after the system message. In replace mode, feedback replaces the system message entirely. Returns the modified prompt. """ feedback = self.read() if not feedback: return prompt if self.mode == "replace": prompt["system"] = feedback else: prompt["system"] = prompt["system"] + "\n\n## Human Guidance\n" + feedback return prompt def set_current_prompt(self, system_prompt: str) -> None: """Store the current system prompt for dashboard visibility.""" self._current_system_prompt = system_prompt def get_current_prompt(self) -> str: """Return the current system prompt.""" return self._current_system_prompt def log_usage(self, iteration: int, feedback_text: str, mode: str) -> None: """Record that feedback was applied at a given iteration.""" entry = { "iteration": iteration, "timestamp": _time.time(), "text": feedback_text, "mode": mode, } self._history.append(entry) logger.info( f"Human feedback logged: iteration={iteration}, mode={mode}, " f"chars={len(feedback_text)}" ) def get_history(self) -> list: """Return the full feedback usage history.""" return list(self._history) def to_serializable(self) -> dict: """Return current state for pickling to Island workers.""" return { "feedback_text": self._last_content, "mode": self.mode, "current_prompt": self._current_system_prompt, } def _write_feedback(self, text: str) -> None: """Write feedback text to the file, preserving the comment header.""" with open(self.path, "w") as f: if text: f.write(_INITIAL_TEMPLATE + "\n" + text + "\n") else: f.write(_INITIAL_TEMPLATE)