Spaces:
Sleeping
Sleeping
File size: 8,884 Bytes
ce8f04a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | """
Strategy cascade recommend — pure, ≥4 paths, no LLM.
Feature flags
-------------
ENABLE_STRATEGY_CASCADE (default: true)
When true (product default), recommend_strategy_paths runs the full 2026
cascade scoring and returns cascade_enforced=true.
When false, the same path list is still produced (scores, ≥4 ids, primary)
but cascade_enforced=false so callers can treat cascade as advisory/off
without losing the path surface.
ENABLE_TAX_PATHS (default: true) — see core.strategy.tax_paths
When true, PATH_TAX (tax_psi_ulgi) gets tax checklists (PSI / ulga B+R /
IP Box) via attach_tax_checklists. When false, PATH_TAX remains in the
cascade without checklist payload; tax_paths_enabled=false on the result.
"""
from __future__ import annotations
import os
from typing import Any, Dict, List, Optional
from core.strategy.direct_eu import attach_direct_eu_meta
from core.strategy.tax_paths import attach_tax_checklists, tax_paths_enabled
# Canonical path ids (product)
PATH_TAX = "tax_psi_ulgi"
PATH_REGIONAL_FST = "regional_fst"
PATH_NATIONAL_FENG = "national_feng"
PATH_DEBT = "debt_loan_guarantee"
PATH_DIRECT_EU = "direct_eu_prep"
FLAG_STRATEGY_CASCADE = "ENABLE_STRATEGY_CASCADE"
def strategy_cascade_enabled() -> bool:
"""Thin flag helper — default ON so product cascade path stays active."""
return os.environ.get(FLAG_STRATEGY_CASCADE, "true").lower() in ("1", "true", "yes")
def recommend_strategy_paths(
*,
eligibility: Optional[Dict[str, Any]] = None,
company_data: Optional[Dict[str, Any]] = None,
goal: str = "",
region: str = "",
) -> Dict[str, Any]:
"""
Returns ordered strategy paths with scores and reasons.
Always returns at least four distinct path labels among the five canonical ids.
cascade_enforced reflects ENABLE_STRATEGY_CASCADE (default true). Paths are
always returned; when the flag is off, cascade_enforced is false.
"""
elig = eligibility or {}
cd = company_data or {}
dm = elig.get("de_minimis") if isinstance(elig.get("de_minimis"), dict) else {}
msp = elig.get("msp") if isinstance(elig.get("msp"), dict) else {}
goal_l = (goal or "").lower()
region_l = (
region
or cd.get("region")
or cd.get("voivodeship")
or (cd.get("address") or "")
)
region_l = str(region_l).lower()
fst_regions = (
"śląsk",
"slask",
"małopol",
"malopol",
"dolnośląsk",
"dolnoslask",
"wielkopol",
"łódzk",
"lodzk",
)
in_fst = any(r in region_l for r in fst_regions)
paths: List[Dict[str, Any]] = []
# Tax / PSI / ulgi — always relevant for investment-like goals
tax_score = 55.0
tax_reasons = ["Ulgi i PSI często pewniejsze niż konkurs grantowy (2026)."]
if any(k in goal_l for k in ("inwestyc", "hala", "linia", "maszyn", "cit", "pit", "podat")):
tax_score += 20
tax_reasons.append("Cel inwestycyjny — sprawdź PSI / ulgę B+R przed naborem.")
if any(k in goal_l for k in ("oprogram", "ip box", "patent", "b+r", "badaw")):
tax_score += 15
tax_reasons.append("B+R/IP — ulga B+R i IP Box jako fundament.")
# Tax checklists attached after primary marking via attach_tax_checklists batch
paths.append(
{
"id": PATH_TAX,
"label": "Podatki / PSI / ulgi (B+R, IP Box)",
"score": tax_score,
"reasons": tax_reasons,
"primary": False,
}
)
# Regional / FST
reg_score = 50.0
reg_reasons = ["Programy regionalne FE — zwykle łatwiejsze wejście dla MŚP."]
if in_fst:
reg_score += 25
reg_reasons.append("Siedziba w obszarze FST — wyższe % i łagodniejsze kryteria.")
if any(k in goal_l for k in ("oze", "termo", "cyfryz", "szkolen", "lokaln")):
reg_score += 10
paths.append(
{
"id": PATH_REGIONAL_FST,
"label": "Region / FST (programy wojewódzkie)",
"score": reg_score,
"reasons": reg_reasons,
"primary": False,
}
)
# National FENG / NCBR grants
feng_score = 52.0
feng_reasons = ["FENG/SMART/NCBR — główny trzon krajowych grantów innowacyjnych."]
if any(k in goal_l for k in ("b+r", "innowac", "trl", "wdroż", "smart", "deeptech")):
feng_score += 22
feng_reasons.append("Cel B+R/innowacja — FENG/SMART lub NCBR.")
if msp.get("is_large"):
feng_score -= 10
feng_reasons.append("Duża firma — część naborów MŚP niedostępna.")
if dm.get("exhausted"):
feng_score -= 5
feng_reasons.append("De minimis wyczerpany — unikaj naborów opartych o de minimis.")
paths.append(
{
"id": PATH_NATIONAL_FENG,
"label": "Krajowe granty FENG / NCBR / PARP",
"score": feng_score,
"reasons": feng_reasons,
"primary": False,
}
)
# Debt / guarantee
debt_score = 48.0
debt_reasons = ["Instrumenty dłużne BGK/ARP — ciągłe nabory, pomost pod grant."]
if dm.get("exhausted"):
debt_score -= 25
debt_reasons.append(
"Limit de minimis wyczerpany — gwarancje de minimis niedostępne jako primary."
)
if any(k in goal_l for k in ("pożyczk", "kredyt", "gwaranc", "płynnoś", "kapitał obrot")):
debt_score += 20
if any(k in goal_l for k in ("kpo",)):
debt_score += 15
debt_reasons.append("KPO 2026: preferuj instrumenty dłużne zamiast nowych grantów KPO.")
paths.append(
{
"id": PATH_DEBT,
"label": "Dłużne / gwarancje (BGK, ARP)",
"score": debt_score,
"reasons": debt_reasons,
"primary": False,
"de_minimis_blocked": bool(dm.get("exhausted")),
}
)
# Direct EU prep
eu_score = 40.0
eu_reasons = ["Direct EU (HE/EIC) lub Granty na Eurogranty jako przygotowanie."]
if any(k in goal_l for k in ("horizon", "eic", "deeptech", "cleantech", "eurogrant", "horyzont")):
eu_score += 30
eu_reasons.append("Cel deeptech/UE — rozważ Horyzont/EIC i Eurogranty prep.")
paths.append(
{
"id": PATH_DIRECT_EU,
"label": "Direct EU / przygotowanie Eurograntu",
"score": eu_score,
"reasons": eu_reasons,
"primary": False,
}
)
# Sort and mark primary (highest that is not de_minimis_blocked when exhausted)
paths.sort(key=lambda p: float(p.get("score") or 0), reverse=True)
primary_set = False
for p in paths:
if p.get("de_minimis_blocked"):
continue
if not primary_set:
p["primary"] = True
primary_set = True
break
if not primary_set and paths:
# all blocked weird case — still mark tax
for p in paths:
if p["id"] == PATH_TAX:
p["primary"] = True
break
# Ensure ≥4 distinct path ids always present
ids = {p["id"] for p in paths}
assert len(ids) >= 4
# Attach tax + Direct EU checklists (F3 / F6 product surface)
paths = [attach_tax_checklists(p) for p in paths]
paths = [attach_direct_eu_meta(p) for p in paths]
# Filter primary recommendation list: do not list de-minimis-blocked as recommended primary
recommended = [p for p in paths if not p.get("de_minimis_blocked")]
if not recommended:
recommended = [p for p in paths if p["id"] == PATH_TAX] or paths[:1]
cascade_on = strategy_cascade_enabled()
tax_on = tax_paths_enabled()
notes = [
"Kolejność kaskady 2026: podatki → region/FST → FENG → dłużne → Direct EU.",
"Wynik jest rekomendacją produktową, nie opinią prawną.",
]
if not cascade_on:
notes.append(
"ENABLE_STRATEGY_CASCADE=false — kaskada nie jest egzekwowana (paths nadal zwracane)."
)
if not tax_on:
notes.append(
"ENABLE_TAX_PATHS=false — checklisty PSI/ulga B+R/IP Box nie dołączone do PATH_TAX."
)
return {
"version": 1,
"paths": paths,
"recommended_paths": recommended[:4],
"primary_path_id": next((p["id"] for p in paths if p.get("primary")), recommended[0]["id"]),
"cascade_order": [
PATH_TAX,
PATH_REGIONAL_FST,
PATH_NATIONAL_FENG,
PATH_DEBT,
PATH_DIRECT_EU,
],
"cascade_enforced": cascade_on,
"tax_paths_enabled": tax_on,
"eligibility_summary": {
"msp": msp.get("status"),
"de_minimis_exhausted": bool(dm.get("exhausted")),
"de_minimis_used_eur": dm.get("used_eur"),
},
"notes": notes,
}
|