import re import time import logging from ai import GeminiProvider logger = logging.getLogger(__name__) TRIGGER_PATTERN = re.compile(r"^BLIGHT:\s+(.+)$", re.MULTILINE) FAILED_TEMPLATE = "" _MAX_RETRIES = 3 _RETRY_DELAYS = [1, 2, 4] # seconds between attempts _provider = GeminiProvider() def process_document(content: str) -> tuple[str, bool]: """Scan content for BLIGHT triggers and process each one. Returns: (updated_content, changed) where changed is True if any triggers were found and the content was modified. """ triggers = list(TRIGGER_PATTERN.finditer(content)) if not triggers: return content, False # Process triggers one by one. After each replacement the string length # may change, so we re-search on the updated content each iteration. changed = False for _ in range(len(triggers)): match = TRIGGER_PATTERN.search(content) if not match: break instruction = match.group(1).strip() trigger_line = match.group(0) logger.info("Processing trigger: %s", instruction) replacement = _call_with_retry(content, instruction) content = content[:match.start()] + replacement + content[match.end():] changed = True return content, changed def _call_with_retry(document: str, instruction: str) -> str: """Call the AI provider with up to _MAX_RETRIES attempts. Returns the AI response on success, or a BLIGHT_FAILED comment on exhausted retries. """ last_error: Exception | None = None for attempt in range(_MAX_RETRIES): try: return _provider.complete(document, instruction) except Exception as exc: last_error = exc if attempt < _MAX_RETRIES - 1: delay = _RETRY_DELAYS[attempt] logger.warning( "Attempt %d/%d failed for instruction %r: %s — retrying in %ds", attempt + 1, _MAX_RETRIES, instruction, exc, delay, ) time.sleep(delay) logger.error( "All %d attempts failed for instruction %r: %s", _MAX_RETRIES, instruction, last_error, ) return FAILED_TEMPLATE.format(instruction=instruction)