"""Generation state — hierarchia źródeł prawdy i helpery persystencji. Problem (audyt): stan generacji był rozproszony w ``external_context`` JSON, checkpointach LangGraph (sqlite) i GSD orchestratorze. Hierarchia odczytu (od najbardziej autorytatywnego): 1. **GSD orchestrator** — ``gsd/gsd_state.py`` + sqlite saver (aktywny run) 2. **Hybrid checkpoint** — ``external_context['orchestrator_checkpoint_v5']`` 3. **Legacy LangGraph generator** — ``data/generator_checkpoints.db`` (DEPRECATED) 4. **Pozostałe klucze w external_context** — cache / pochodne (nie źródło prawdy) Zapis: używaj ``persist_generation_checkpoint`` aby synchronizować warstwy 1↔2. """ from __future__ import annotations import logging from copy import deepcopy from typing import Any logger = logging.getLogger(__name__) CHECKPOINT_V5_KEY = "orchestrator_checkpoint_v5" GSD_STATE_KEY = "gsd_state" GENERATION_META_KEY = "generation_meta" DERIVED_CONTEXT_KEYS = frozenset( { "holistic_review", "risk_score", "blocks_export_recommendation", "v5_grounding_certificate", "compliance_checklist", "regulation_section_titles", "credibility_flags", } ) def get_authoritative_checkpoint(project) -> dict[str, Any] | None: """Zwraca hybrid checkpoint v5 z projektu lub None.""" ext = getattr(project, "external_context", None) or {} ck = ext.get(CHECKPOINT_V5_KEY) return ck if isinstance(ck, dict) else None def get_generation_meta(project) -> dict[str, Any]: """Metadane generacji (ścieżka orkiestracji, ostatnia faza).""" ext = getattr(project, "external_context", None) or {} meta = ext.get(GENERATION_META_KEY) or {} return meta if isinstance(meta, dict) else {} def persist_generation_checkpoint( db, project, checkpoint: dict[str, Any], *, orchestration_path: str = "gsd", merge_meta: dict[str, Any] | None = None, ) -> None: """Zapisuje checkpoint do ``external_context`` (single durable source w Postgres/SQLite).""" from sqlalchemy.orm.attributes import flag_modified ext = deepcopy(getattr(project, "external_context", None) or {}) ext[CHECKPOINT_V5_KEY] = checkpoint meta = dict(ext.get(GENERATION_META_KEY) or {}) meta["orchestration_path"] = orchestration_path meta["last_checkpoint_stage"] = checkpoint.get("stage") or checkpoint.get("current_orchestrator_stage") if merge_meta: meta.update(merge_meta) ext[GENERATION_META_KEY] = meta project.external_context = ext flag_modified(project, "external_context") db.commit() logger.debug( "[generation_state] Persisted checkpoint stage=%s path=%s project=%s", meta.get("last_checkpoint_stage"), orchestration_path, getattr(project, "id", "?"), ) def is_legacy_langgraph_checkpoint(config: dict[str, Any] | None) -> bool: """Czy config wskazuje na legacy LangGraph thread (generator agent).""" if not config: return False tid = config.get("configurable", {}).get("thread_id", "") return isinstance(tid, str) and tid.startswith("gen-")