Skip to content

Instantly share code, notes, and snippets.

@armandocanals
Last active August 13, 2026 15:23
Show Gist options
  • Select an option

  • Save armandocanals/393725877bbb8c931bd5c7d595e43e96 to your computer and use it in GitHub Desktop.

Select an option

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
"""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