grantforge-api / backend /core /strategy /direct_eu.py
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
4.98 kB
"""
Direct EU prep helpers (PLAN_ECOSYSTEM F6) — pure, no network.
Product surface for Horizon Europe / EIC preparation checklists and grant
classification. Must never map Direct EU prep to SMART PL multi-module structure.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
# Canonical strategy path id (core.strategy.recommend.PATH_DIRECT_EU)
PATH_DIRECT_EU = "direct_eu_prep"
# Advisor checklist id (not a submission form)
CHECKLIST_ID = "horizon_prep"
_HORIZON_MARKERS = (
"horizon europe",
"horyzont europa",
"horyzont",
"horizon",
"eic",
"eic accelerator",
"pathfinder",
"transition",
"digital europe",
"heu",
"work programme",
"funding & tenders",
"funding and tenders",
)
# Strong Direct EU program families (explicit — never SMART modules)
_DIRECT_EU_FAMILIES = frozenset(
{
"HORIZON_PREP",
"HORIZON",
"EIC",
"EUROGRANTY",
"DIRECT_EU",
}
)
_SMART_PL_MARKERS = (
"ścieżka smart",
"sciezka smart",
"feng.01",
"moduł b+r smart",
"modul b+r smart",
)
_STRONG_EU_MARKERS = (
"horizon europe",
"horyzont europa",
"eic accelerator",
"digital europe",
)
_PREP_CHECKLIST = [
"Ustal call / Work Programme i deadline w Funding & Tenders Portal.",
"Sprawdź TRL i typ działania (RIA/IA/CSA / EIC Accelerator).",
"Zdefiniuj konsorcjum lub tryb single-beneficiary (EIC).",
"Przygotuj Concept Note / short proposal wg szablonu callu.",
"Oceń potrzebę Granty na Eurogranty (PARP) jako przygotowanie — osobny tor PL.",
"NIE zakładaj struktury Ścieżki SMART FENG.01 jako domyślnej dla naboru UE.",
]
_PATH_NOTES = [
"Direct EU ≠ Ścieżka SMART. Schema/family must not inherit SMART multi-module structure.",
"Eurogranty (PARP FENG.02.12) to tor przygotowawczy PL — nie mylić z samym call Horizon/EIC.",
]
def horizon_prep_checklist() -> List[str]:
"""Advisor checklist for Direct EU / Horizon prep (not a submission form)."""
return list(_PREP_CHECKLIST)
def _blob(grant: Dict[str, Any]) -> str:
parts = [
str(grant.get("name") or ""),
str(grant.get("title") or ""),
str(grant.get("program") or ""),
str(grant.get("program_name") or ""),
str(grant.get("program_type") or ""),
str(grant.get("description") or "")[:1500],
str(grant.get("instrument_type") or ""),
str(grant.get("source") or ""),
str(grant.get("family") or ""),
" ".join(str(t) for t in (grant.get("tags") or []) if t),
]
return " ".join(parts).lower()
def is_direct_eu_grant(grant: Optional[Dict[str, Any]]) -> bool:
"""
True if grant looks like Horizon / EIC / Direct EU / Eurogranty prep.
Never classifies pure SMART PL (Ścieżka SMART / FENG.01) as Direct EU
unless strong Horizon/EIC markers are also present.
"""
if not isinstance(grant, dict) or not grant:
return False
fam = str(grant.get("family") or grant.get("program_type") or "").strip().upper()
if grant.get("direct_eu") is True or fam in _DIRECT_EU_FAMILIES:
return True
b = _blob(grant)
smartish = any(m in b for m in _SMART_PL_MARKERS)
strong_eu = any(m in b for m in _STRONG_EU_MARKERS)
if smartish and not strong_eu:
return False
return any(m in b for m in _HORIZON_MARKERS)
def never_maps_to_smart_pl(grant: Optional[Dict[str, Any]] = None) -> bool:
"""
Product invariant: Direct EU prep path and classified Direct EU grants
must not use SMART PL multi-module structure.
"""
if grant is None:
return True
if is_direct_eu_grant(grant):
return True
return False
def direct_eu_prep_path_meta() -> Dict[str, Any]:
"""Attachable metadata for strategy PATH_DIRECT_EU."""
return {
"id": PATH_DIRECT_EU,
"checklist_id": CHECKLIST_ID,
"checklist": horizon_prep_checklist(),
"never_use_smart_modules": True,
"notes": list(_PATH_NOTES),
}
def attach_direct_eu_meta(path: Dict[str, Any]) -> Dict[str, Any]:
"""
Attach Direct EU prep checklist onto a strategy path dict when id matches.
Returns a new dict; does not mutate the input. Non-Direct-EU paths get a
shallow copy without Direct EU fields.
"""
out = dict(path) if isinstance(path, dict) else {}
if str(out.get("id") or "") != PATH_DIRECT_EU:
return out
meta = direct_eu_prep_path_meta()
out["checklist_id"] = meta["checklist_id"]
out["checklist"] = list(meta["checklist"])
out["never_use_smart_modules"] = True
out["notes"] = list(meta["notes"])
# Surface guardrail in reasons for cascade UI consumers
reasons = list(out.get("reasons") or [])
guard = "Direct EU ≠ SMART PL — nie używaj multi-modułów FENG.01."
if guard not in reasons:
reasons.append(guard)
out["reasons"] = reasons
return out