Spaces:
Sleeping
Sleeping
| """ | |
| Generation consent + dossier gate (F1). | |
| grounding_mode: | |
| - regulation: normal grounded generation | |
| - structure_only: user explicitly allowed generation without regulation pack | |
| - blocked: blind dossier and no valid consent | |
| Env: | |
| REQUIRE_DOSSIER_OR_CONSENT=true (default) — enforce gate | |
| ALLOW_STRUCTURE_WITHOUT_REGULATION=true (default) — allow consent path | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from datetime import datetime, timezone | |
| from typing import Any, Dict, Optional, Tuple | |
| CONSENT_MODE_STRUCTURE = "structure_without_regulation" | |
| GROUNDING_REGULATION = "regulation" | |
| GROUNDING_STRUCTURE = "structure_only" | |
| GROUNDING_BLOCKED = "blocked" | |
| ERROR_CODE = "REGULATION_DOSSIER_REQUIRED" | |
| STRUCTURE_EXPORT_REASON = ( | |
| "Tryb strukturalny (brak regulaminu w bazie wiedzy) — wymagana weryfikacja człowieka " | |
| "przed złożeniem wniosku w instytucji." | |
| ) | |
| STRUCTURE_ONLY_PROMPT = """ | |
| === TRYB STRUKTURALNY (BEZ REGULAMINU NABORU W SYSTEMIE) === | |
| Użytkownik jawnie zgodził się na generację SZKIELETU wniosku bez ugruntowania w regulaminie. | |
| OBOWIĄZKOWE ZASADY: | |
| 1. NIE cytuj regulaminu naboru, NIE udawaj § / numerów aktów, których nie masz w kontekście. | |
| 2. Każdą regułę programową oznacz: [DO WERYFIKACJI: regulamin naboru]. | |
| 3. Opieraj się wyłącznie na opisie projektu, danych firmy i ogólnym szablonie sekcji. | |
| 4. Nie twierdź, że treść jest zgodna z wymogami operatora. | |
| 5. Preferuj konserwatywne sformułowania i listy braków do uzupełnienia. | |
| """ | |
| def require_dossier_or_consent_enabled() -> bool: | |
| return os.environ.get("REQUIRE_DOSSIER_OR_CONSENT", "true").lower() in ( | |
| "1", | |
| "true", | |
| "yes", | |
| ) | |
| def allow_structure_without_regulation() -> bool: | |
| return os.environ.get("ALLOW_STRUCTURE_WITHOUT_REGULATION", "true").lower() in ( | |
| "1", | |
| "true", | |
| "yes", | |
| ) | |
| def _dossier_level(ext: Dict[str, Any]) -> str: | |
| """best-effort readiness level from external_context.""" | |
| pd = ext.get("program_dossier") if isinstance(ext.get("program_dossier"), dict) else {} | |
| readiness = pd.get("readiness") if isinstance(pd.get("readiness"), dict) else {} | |
| level = (readiness.get("level") or "").lower().strip() | |
| if level in ("blind", "partial", "ready"): | |
| return level | |
| # Infer from documents / regulation URL | |
| docs = ext.get("regulation_documents") or pd.get("regulation_documents") or [] | |
| primary = ( | |
| ext.get("precise_regulation_url") | |
| or ext.get("regulation_url") | |
| or "" | |
| ) | |
| from core.grants.program_dossier import dossier_readiness | |
| if isinstance(docs, list) and docs: | |
| r = dossier_readiness(docs, str(primary)) | |
| return r.get("level") or "blind" | |
| if primary and str(primary).startswith("http"): | |
| # single URL may still be announcement — treat as partial if present | |
| try: | |
| from core.grants.regulation_url_quality import is_valid_regulation_url | |
| if is_valid_regulation_url(str(primary)) or str(primary).lower().endswith(".pdf"): | |
| return "partial" | |
| except Exception: | |
| if str(primary).lower().endswith(".pdf"): | |
| return "partial" | |
| return "blind" | |
| def has_valid_structure_consent(ext: Dict[str, Any]) -> bool: | |
| consent = ext.get("generation_consent") | |
| if not isinstance(consent, dict): | |
| return False | |
| if consent.get("mode") != CONSENT_MODE_STRUCTURE: | |
| return False | |
| if not consent.get("granted_at"): | |
| return False | |
| # Consent only valid while still blind at decision time OR still structure mode requested | |
| # After non-blind, resolve_grounding_mode ignores consent | |
| return True | |
| def resolve_grounding_mode(external_context: Optional[Dict[str, Any]]) -> str: | |
| """Pure decision: regulation | structure_only | blocked.""" | |
| ext = dict(external_context or {}) | |
| if not require_dossier_or_consent_enabled(): | |
| return GROUNDING_REGULATION | |
| level = _dossier_level(ext) | |
| if level in ("ready", "partial"): | |
| return GROUNDING_REGULATION | |
| # blind | |
| if has_valid_structure_consent(ext) and allow_structure_without_regulation(): | |
| return GROUNDING_STRUCTURE | |
| return GROUNDING_BLOCKED | |
| def consent_required(external_context: Optional[Dict[str, Any]]) -> bool: | |
| if not require_dossier_or_consent_enabled(): | |
| return False | |
| return resolve_grounding_mode(external_context) == GROUNDING_BLOCKED | |
| def build_generation_gate(external_context: Optional[Dict[str, Any]]) -> Dict[str, Any]: | |
| """ | |
| Returns: | |
| allowed: bool | |
| grounding_mode: str | |
| dossier_readiness: str | |
| consent_required: bool | |
| code: optional error code | |
| message: human message | |
| structure_prompt: optional injection for structure_only | |
| """ | |
| ext = dict(external_context or {}) | |
| level = _dossier_level(ext) | |
| mode = resolve_grounding_mode(ext) | |
| out: Dict[str, Any] = { | |
| "allowed": mode != GROUNDING_BLOCKED, | |
| "grounding_mode": mode, | |
| "dossier_readiness": level, | |
| "consent_required": mode == GROUNDING_BLOCKED, | |
| "code": None, | |
| "message": "", | |
| "structure_prompt": None, | |
| "human_review_required": mode == GROUNDING_STRUCTURE, | |
| "export_reason": STRUCTURE_EXPORT_REASON if mode == GROUNDING_STRUCTURE else None, | |
| "require_flag_on": require_dossier_or_consent_enabled(), | |
| "allow_structure_flag_on": allow_structure_without_regulation(), | |
| } | |
| if mode == GROUNDING_BLOCKED: | |
| out["code"] = ERROR_CODE | |
| out["message"] = ( | |
| "Brak regulaminu naboru w systemie (dossier: blind). " | |
| "Dołącz regulamin (URL/PDF) albo jawnie zaakceptuj generację struktury bez reguł." | |
| ) | |
| elif mode == GROUNDING_STRUCTURE: | |
| out["message"] = ( | |
| "Tryb strukturalny: generacja bez ugruntowania w regulaminie (zgoda użytkownika)." | |
| ) | |
| out["structure_prompt"] = STRUCTURE_ONLY_PROMPT.strip() | |
| else: | |
| out["message"] = "Generacja ugruntowana w dokumentach naboru." | |
| return out | |
| def apply_structure_consent( | |
| external_context: Optional[Dict[str, Any]], | |
| *, | |
| ack: bool, | |
| mode: str, | |
| clerk_user_id: str, | |
| grant_id: str = "", | |
| ) -> Tuple[Dict[str, Any], Optional[str]]: | |
| """ | |
| Returns (new_ext, error_message). | |
| error_message set on validation failure. | |
| """ | |
| if not ack: | |
| return dict(external_context or {}), "Wymagane jawne potwierdzenie (ack=true)." | |
| if mode != CONSENT_MODE_STRUCTURE: | |
| return dict(external_context or {}), f"Nieobsługiwany mode={mode}." | |
| if not allow_structure_without_regulation(): | |
| return dict(external_context or {}), "Generacja bez regulaminu jest wyłączona (ALLOW_STRUCTURE_WITHOUT_REGULATION)." | |
| ext = dict(external_context or {}) | |
| level = _dossier_level(ext) | |
| if level in ("ready", "partial"): | |
| # No need for structure consent — force regulation | |
| ext.pop("generation_consent", None) | |
| ext["grounding_mode"] = GROUNDING_REGULATION | |
| return ext, "Dossier nie jest blind — zgoda na structure_only nie jest potrzebna." | |
| ext["generation_consent"] = { | |
| "mode": CONSENT_MODE_STRUCTURE, | |
| "granted_at": datetime.now(timezone.utc).isoformat(), | |
| "granted_by": clerk_user_id or "unknown", | |
| "dossier_level_at_consent": level, | |
| "grant_id": grant_id | |
| or str((ext.get("selected_grant") or {}).get("id") or ext.get("grant_id") or ""), | |
| "version": 1, | |
| } | |
| ext["grounding_mode"] = GROUNDING_STRUCTURE | |
| return ext, None | |
| def clear_structure_consent(external_context: Optional[Dict[str, Any]]) -> Dict[str, Any]: | |
| ext = dict(external_context or {}) | |
| ext.pop("generation_consent", None) | |
| # recompute mode | |
| mode = resolve_grounding_mode(ext) | |
| ext["grounding_mode"] = mode if mode != GROUNDING_BLOCKED else GROUNDING_BLOCKED | |
| return ext | |
| def sync_grounding_after_dossier_update(external_context: Optional[Dict[str, Any]]) -> Dict[str, Any]: | |
| """ | |
| Call after regulation attach / dossier ensure. | |
| If no longer blind, drop structure consent and set regulation mode. | |
| """ | |
| ext = dict(external_context or {}) | |
| level = _dossier_level(ext) | |
| if level in ("ready", "partial"): | |
| ext.pop("generation_consent", None) | |
| ext["grounding_mode"] = GROUNDING_REGULATION | |
| # store snapshot of readiness for GET | |
| pd = ext.get("program_dossier") if isinstance(ext.get("program_dossier"), dict) else {} | |
| readiness = pd.get("readiness") if isinstance(pd.get("readiness"), dict) else {} | |
| if not readiness: | |
| pd = dict(pd) | |
| pd["readiness"] = {"level": level} | |
| ext["program_dossier"] = pd | |
| return ext | |
| mode = resolve_grounding_mode(ext) | |
| ext["grounding_mode"] = mode | |
| return ext | |
| def project_generation_meta(external_context: Optional[Dict[str, Any]]) -> Dict[str, Any]: | |
| """Fields to expose on GET project.""" | |
| gate = build_generation_gate(external_context) | |
| ext = dict(external_context or {}) | |
| return { | |
| "grounding_mode": gate["grounding_mode"], | |
| "dossier_readiness": gate["dossier_readiness"], | |
| "consent_required": gate["consent_required"], | |
| "generation_allowed": gate["allowed"], | |
| "generation_consent": ext.get("generation_consent"), | |
| "generation_gate_message": gate["message"], | |
| } | |