Last active
August 13, 2026 15:23
-
-
Save armandocanals/393725877bbb8c931bd5c7d595e43e96 to your computer and use it in GitHub Desktop.
Runner-ready Python to comprehensively backfill missing case-readout HTML and safely retry blank Notion publications
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Repair current case-readout HTML artifacts and blank Notion publications. | |
| Paste this file into Finch's production Python runner. It is read-only by | |
| default. Run it once with APPLY = False, copy the desired run IDs from the JSON | |
| output into RUN_IDS, then set APPLY = True and run it again. | |
| The script reuses completed bulk FinchGPT output. It never reruns FinchGPT. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from uuid import UUID | |
| import httpx | |
| from asgiref.sync import async_to_sync | |
| from django.db.models import Q | |
| from django.utils import timezone | |
| from app.clients.notion import NotionClient | |
| from app.clients.ssm import SSMClient | |
| from app.defs.agent_chat import BulkAgentChatOperationStatus | |
| from app.defs.firm_readout.firm_readout import ( | |
| FirmBulkAgentChatRunStatus, | |
| FirmBulkAgentChatRunStep, | |
| ) | |
| from app.defs.ssm import SSMParams | |
| from app.domain.firm_readout.automation import start_firm_readout_publish_workflow | |
| from app.domain.firm_readout.case_field_catalog import build_case_readout_case_row | |
| from app.domain.firm_readout.html_readout import ( | |
| HTML_READOUT_SPEC_VERSION, | |
| build_html_readout_case, | |
| html_readout_filename, | |
| render_case_readout_html, | |
| ) | |
| from app.models.firm_readout import FirmBulkAgentChatRun | |
| from app.queries.firm_readout import ( | |
| case_readout_html_artifact_exists, | |
| get_bulk_agent_chat_run_template_config, | |
| get_firm_bulk_agent_chat_run, | |
| load_bulk_operation_for_push, | |
| persist_case_readout_html, | |
| update_bulk_agent_chat_run_state, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # EDIT THESE VALUES | |
| # --------------------------------------------------------------------------- | |
| APPLY = False | |
| LIMIT = 500 | |
| RUN_IDS: list[str] = [] | |
| # Example: | |
| # RUN_IDS = ["3bbe7a32-2002-81e3-ba96-e0562c8282c8"] | |
| # --------------------------------------------------------------------------- | |
| if LIMIT < 1: | |
| raise SystemExit("LIMIT must be at least 1") | |
| validated_run_ids = [str(UUID(value)) for value in RUN_IDS] | |
| if APPLY and not validated_run_ids: | |
| raise SystemExit( | |
| "Refusing an unscoped production repair. Run with APPLY = False first, " | |
| "then paste the desired IDs into RUN_IDS and set APPLY = True." | |
| ) | |
| def ensure_html_artifact(run: FirmBulkAgentChatRun) -> tuple[str, int | None]: | |
| """Generate the current deployed renderer's artifact without changing run status.""" | |
| if case_readout_html_artifact_exists( | |
| run_id=run.id, | |
| spec_version=HTML_READOUT_SPEC_VERSION, | |
| ): | |
| return "already_available", None | |
| if not APPLY: | |
| return "would_generate", None | |
| if run.bulk_operation_id is None: | |
| raise ValueError("Run has no completed bulk operation to render") | |
| operation = load_bulk_operation_for_push( | |
| run.bulk_operation_id, | |
| include_attorney_review_enrichment=False, | |
| ) | |
| template_config = get_bulk_agent_chat_run_template_config(run.id) | |
| html_cases = [] | |
| for thread in operation.threads: | |
| case = build_case_readout_case_row(template_config, thread) | |
| html_cases.append(build_html_readout_case(template_config, thread, case)) | |
| meeting_at = run.meeting_at or timezone.now() | |
| as_of = meeting_at.astimezone(timezone.get_current_timezone()).date() | |
| content = render_case_readout_html( | |
| firm_name=operation.firm_name, | |
| as_of=as_of, | |
| config=template_config, | |
| cases=html_cases, | |
| ).encode("utf-8") | |
| persisted = persist_case_readout_html( | |
| run_id=run.id, | |
| filename=html_readout_filename(operation.firm_name, as_of), | |
| content=content, | |
| spec_version=HTML_READOUT_SPEC_VERSION, | |
| ) | |
| if not persisted: | |
| raise ValueError("Run disappeared before its HTML artifact could be persisted") | |
| return "generated", len(html_cases) | |
| notion_recovery_statuses = [ | |
| FirmBulkAgentChatRunStatus.RUNNING.value, | |
| FirmBulkAgentChatRunStatus.NEEDS_ATTENTION.value, | |
| FirmBulkAgentChatRunStatus.FAILED.value, | |
| ] | |
| html_needs_backfill = ( | |
| Q(html_readout__isnull=True) | |
| | Q(html_readout="") | |
| | ~Q(html_readout_spec_version=HTML_READOUT_SPEC_VERSION) | |
| ) | |
| notion_needs_recovery = Q( | |
| publish_notion=True, | |
| status__in=notion_recovery_statuses, | |
| current_step=FirmBulkAgentChatRunStep.NOTION_PUBLISH.value, | |
| notion_database_ids={}, | |
| slack_message_ts="", | |
| ) | |
| queryset = ( | |
| FirmBulkAgentChatRun.objects.select_related("bulk_operation", "firm") | |
| .filter( | |
| bulk_operation__status=BulkAgentChatOperationStatus.COMPLETED.value, | |
| ) | |
| .filter(html_needs_backfill | notion_needs_recovery) | |
| .order_by("-created_at") | |
| ) | |
| if validated_run_ids: | |
| queryset = queryset.filter(id__in=validated_run_ids) | |
| matching_count = queryset.count() | |
| candidates = list(queryset[:LIMIT]) | |
| notion_client: NotionClient | None = None | |
| results: list[dict[str, object]] = [] | |
| try: | |
| for run in candidates: | |
| item: dict[str, object] = { | |
| "run_id": str(run.id), | |
| "firm": run.firm.name, | |
| "created_at": run.created_at.isoformat(), | |
| "meeting_at": run.meeting_at.isoformat() if run.meeting_at else None, | |
| "status": run.status, | |
| "current_step": run.current_step, | |
| "notion_page_id": run.notion_page_id, | |
| "html_spec_version": HTML_READOUT_SPEC_VERSION, | |
| "attention_reason": run.attention_reason, | |
| "last_error": run.last_error, | |
| } | |
| try: | |
| html_result, case_count = ensure_html_artifact(run) | |
| item["html_result"] = html_result | |
| if case_count is not None: | |
| item["html_case_count"] = case_count | |
| except Exception as exc: | |
| item["html_result"] = "generation_failed" | |
| item["html_error"] = f"{type(exc).__name__}: {exc}" | |
| item["notion_result"] = "skipped_until_html_is_available" | |
| results.append(item) | |
| continue | |
| is_notion_recovery_candidate = bool( | |
| run.publish_notion | |
| and run.status in notion_recovery_statuses | |
| and run.current_step == FirmBulkAgentChatRunStep.NOTION_PUBLISH.value | |
| and not run.notion_database_ids | |
| and not run.slack_message_ts | |
| ) | |
| if not is_notion_recovery_candidate: | |
| item["notion_result"] = "not_applicable" | |
| results.append(item) | |
| continue | |
| page_is_missing = not bool(run.notion_page_id) | |
| page_is_blank = page_is_missing | |
| if run.notion_page_id: | |
| try: | |
| if notion_client is None: | |
| token = SSMClient().get_param(SSMParams.notion_api_token()) | |
| if not token.strip(): | |
| raise ValueError("NOTION_API_TOKEN is empty") | |
| notion_client = NotionClient(api_key=token) | |
| children = notion_client.get_block_children(run.notion_page_id) | |
| page_is_blank = not bool(children) | |
| if children: | |
| item["notion_result"] = "skipped_page_not_blank" | |
| item["notion_block_count"] = len(children) | |
| results.append(item) | |
| continue | |
| except httpx.HTTPStatusError as exc: | |
| if exc.response.status_code != 404: | |
| item["notion_result"] = f"skipped_notion_http_{exc.response.status_code}" | |
| results.append(item) | |
| continue | |
| page_is_missing = True | |
| page_is_blank = True | |
| except Exception as exc: | |
| item["notion_result"] = "inspection_failed" | |
| item["notion_error"] = f"{type(exc).__name__}: {exc}" | |
| results.append(item) | |
| continue | |
| if run.status == FirmBulkAgentChatRunStatus.RUNNING.value: | |
| item["notion_result"] = "skipped_active_workflow" | |
| results.append(item) | |
| continue | |
| if not APPLY: | |
| item["notion_result"] = ( | |
| "would_restart_missing_page" if page_is_missing else "would_restart_blank_page" | |
| ) | |
| results.append(item) | |
| continue | |
| if not page_is_missing and page_is_blank and notion_client is not None: | |
| notion_client.archive_page(run.notion_page_id) | |
| FirmBulkAgentChatRun.objects.filter(id=run.id).update( | |
| notion_page_id="", | |
| notion_page_url="", | |
| notion_database_ids={}, | |
| modified_at=timezone.now(), | |
| ) | |
| update_bulk_agent_chat_run_state( | |
| run_id=run.id, | |
| status=FirmBulkAgentChatRunStatus.RUNNING, | |
| current_step=FirmBulkAgentChatRunStep.NOTION_PUBLISH, | |
| attention_reason="", | |
| last_error="", | |
| ) | |
| refreshed = get_firm_bulk_agent_chat_run(run.id) | |
| if refreshed is None: | |
| item["notion_result"] = "run_disappeared" | |
| results.append(item) | |
| continue | |
| try: | |
| summary = async_to_sync(start_firm_readout_publish_workflow)(refreshed) | |
| except Exception as exc: | |
| update_bulk_agent_chat_run_state( | |
| run_id=run.id, | |
| status=FirmBulkAgentChatRunStatus.NEEDS_ATTENTION, | |
| attention_reason=( | |
| "Repair generated HTML but could not restart Notion publication." | |
| ), | |
| last_error=str(exc), | |
| ) | |
| item["notion_result"] = "reset_but_restart_failed" | |
| item["notion_error"] = f"{type(exc).__name__}: {exc}" | |
| else: | |
| item["notion_result"] = "restarted" | |
| item["workflow_id"] = summary.workflow_id | |
| results.append(item) | |
| finally: | |
| if notion_client is not None: | |
| notion_client.close() | |
| print( | |
| json.dumps( | |
| { | |
| "dry_run": not APPLY, | |
| "matching_count": matching_count, | |
| "candidate_count": len(candidates), | |
| "truncated": matching_count > len(candidates), | |
| "html_generated_count": sum(item.get("html_result") == "generated" for item in results), | |
| "html_would_generate_count": sum( | |
| item.get("html_result") == "would_generate" for item in results | |
| ), | |
| "notion_restarted_count": sum( | |
| item.get("notion_result") == "restarted" for item in results | |
| ), | |
| "notion_would_restart_count": sum( | |
| str(item.get("notion_result", "")).startswith("would_restart") for item in results | |
| ), | |
| "results": results, | |
| }, | |
| indent=2, | |
| sort_keys=True, | |
| default=str, | |
| ) | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment