grantforge-api / backend /core /projects /instrument_schema.py
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
35.9 kB
"""
Instrument schema: what a grant instrument actually funds and requires.
Source priority:
1) Gold profile (known instrument family)
2) Heuristic extract from regulation / announcement text
3) Conservative generic (never invents SMART multi-modules)
"""
from __future__ import annotations
import re
from typing import Any, Dict, List, Optional, Sequence
from core.projects.instrument_profile import (
EUROGRANTY_GENERATION_RULES,
KREDYT_EKO_GENERATION_RULES,
KREDYT_TECH_GENERATION_RULES,
STEP_GENERATION_RULES,
get_instrument_profile,
resolve_program_type,
)
from core.grants.advisor_brief_extract import (
build_advisor_brief_from_text,
extract_key_rules_from_text,
)
SCHEMA_VERSION = 1
# ---------------------------------------------------------------------------
# Gold profiles (curated — extend over time; extract fills gaps)
# ---------------------------------------------------------------------------
_GOLD: Dict[str, Dict[str, Any]] = {
"EUROGRANTY": {
"family": "EUROGRANTY",
"instrument_kind": "preparatory_lump_sum",
"label": "Granty na Eurogranty (FENG.02.12)",
"funds_rnd": False,
"funds_infrastructure": False,
"funds_implementation": False,
"budget_form": "lump_sum",
"required_sections": [
"Informacje o projekcie i target Eurograntu (program UE, call, WP, termin)",
"Wnioskodawca i kwalifikowalność (MŚP, de minimis, rok obrotowy, PKD)",
"Opis działań przygotowawczych (partnerzy, pisanie wniosku, tłumaczenia, analizy)",
"Rola w Eurograncie i partnerstwo (samodzielny / koordynator / członek)",
"Zespół przygotowujący wniosek i kompetencje (powiązanie z realnym PKD firmy)",
"Budżet ryczałtowy (jedna kwota wg roli + analizy specjalistyczne)",
"Harmonogram powiązany z terminem konkursu UE",
"Wskaźniki: złożenie 1 wniosku Eurogrant + wynik oceny >50%",
"Zasady horyzontalne, DNSH, KIS, zrównoważony rozwój",
"Załączniki i oświadczenia",
],
"allowed_costs": [
"poszukiwanie partnerów i spotkania",
"przygotowanie wniosku o Eurogrant",
"tłumaczenia",
"analizy specjalistyczne (uzasadnione)",
"wynagrodzenia / podróże / doradztwo przygotowawcze",
],
"forbidden_costs": [
"prace B+R / rozwój produktu",
"infrastruktura B+R",
"wdrożenie innowacji produkcyjnej",
"moduły SMART (Cyfryzacja, Zazielenienie, Kompetencje)",
"budowa fabryki / platformy jako przedmiot projektu",
],
"obligatory_criteria": [
"kwalifikowalność wnioskodawcy (MŚP / org. badawcza, rok obrotowy ≥12 mies.)",
"kwalifikowalność i zasadność projektu (konkretny target Eurograntu)",
"budżet = dopuszczalna kwota ryczałtowa",
"wskaźniki zgodne z formularzem (złożenie wniosku + wynik >50%)",
],
"indicators": [
"złożenie 1 wniosku o Eurogrant po dacie wniosku do PARP",
"wynik oceny Eurograntu >50% punktów/etapów",
],
"required_company_fields": [
"nip",
"company_name",
"pkd_codes",
"msp_status",
"de_minimis",
"closed_financial_year",
"revenue_3y",
"employment_fte",
],
"required_project_fields": [
"eurogrant_programme",
"eurogrant_call",
"eurogrant_role",
"specialist_analysis_yes_no",
"lump_sum_amount_pln",
],
"allow_smart_modules": False,
"generation_rules": EUROGRANTY_GENERATION_RULES,
},
"SMART": {
"family": "SMART",
"instrument_kind": "innovation_modules",
"label": "Ścieżka SMART / FENG innowacje",
"funds_rnd": True,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Informacje ogólne o projekcie",
"Wnioskodawca i powiązania",
"Zespół zarządzający i projektowy",
"Analiza zapotrzebowania i rynku",
"Innowacyjność (TRL) i znaczenie projektu",
"Moduł B+R: Plan prac",
"Moduł Wdrożenie Innowacji",
"Zrównoważony rozwój i zasada DNSH",
"Montaż finansowy i koszty",
"Wskaźniki KPI i analiza ryzyka",
"Załączniki i Oświadczenia",
],
"allowed_costs": ["koszty kwalifikowalne wg regulaminu SMART/FENG"],
"forbidden_costs": [],
"obligatory_criteria": [
"innowacyjność i TRL",
"kwalifikowalność wnioskodawcy",
"DNSH / zasady horyzontalne",
],
"indicators": ["wskaźniki produktu i rezultatu wg regulaminu"],
"required_company_fields": [
"nip",
"company_name",
"pkd_codes",
"msp_status",
],
"required_project_fields": [
"project_description",
"innovation_description",
],
"allow_smart_modules": True,
"generation_rules": "",
},
"ARIMR": {
"family": "ARIMR",
"instrument_kind": "agricultural_investment",
"label": "ARiMR / inwestycje rolnicze",
"funds_rnd": False,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Opis inwestycji",
"Dobrostan zwierząt / modernizacja",
"Analiza opłacalności",
"Środowisko i warunki",
"Budżet i wkład własny",
"Załączniki",
],
"allowed_costs": ["inwestycje kwalifikowalne wg naboru ARiMR"],
"forbidden_costs": ["prace B+R przemysłowe spoza zakresu naboru"],
"obligatory_criteria": ["kwalifikowalność gospodarstwa / wnioskodawcy"],
"indicators": [],
"required_company_fields": ["nip", "company_name"],
"required_project_fields": ["project_description"],
"allow_smart_modules": False,
"generation_rules": (
"Instrument rolniczy ARiMR — nie generuj modułów SMART B+R / Cyfryzacja."
),
},
"ZUS_BHP": {
"family": "ZUS_BHP",
"instrument_kind": "workplace_safety",
"label": "ZUS dofinansowanie BHP",
"funds_rnd": False,
"funds_infrastructure": False,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Opis projektu BHP",
"Zakres działań i kosztów",
"Harmonogram",
"Efekty i wskaźniki BHP",
"Załączniki",
],
"allowed_costs": ["koszty kwalifikowalne BHP wg regulaminu ZUS"],
"forbidden_costs": ["moduły SMART / Eurogranty"],
"obligatory_criteria": ["kwalifikowalność płatnika składek"],
"indicators": [],
"required_company_fields": ["nip", "company_name"],
"required_project_fields": ["project_description"],
"allow_smart_modules": False,
"generation_rules": "Instrument ZUS BHP — nie generuj struktury SMART ani Eurograntów.",
"legal_references": [],
},
"BGK": {
"family": "BGK",
"instrument_kind": "financial_instrument",
"label": "BGK — instrumenty finansowe / gwarancje / preferencyjne finansowanie",
"funds_rnd": False,
"funds_infrastructure": False,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Informacje o wniosku i produkcie BGK",
"Wnioskodawca i zdolność formalna",
"Cel i zakres finansowania",
"Zabezpieczenia / warunki produktu (jeśli dotyczy)",
"Plan spłaty / wykorzystania środków",
"Załączniki i oświadczenia",
],
"allowed_costs": ["zgodnie z regulaminem konkretnego produktu BGK"],
"forbidden_costs": [
"moduły SMART B+R",
"budżet ryczałtowy Eurograntów (inny instrument)",
],
"obligatory_criteria": [
"kwalifikowalność wnioskodawcy wg produktu BGK",
"zgodność celu z regulaminem produktu",
],
"indicators": [],
"required_company_fields": ["nip", "company_name", "msp_status"],
"required_project_fields": ["project_description"],
"allow_smart_modules": False,
"generation_rules": (
"Instrument BGK (gwarancja/pożyczka/preferencyjne finansowanie) — "
"NIE generuj modułów SMART ani struktury Grantów na Eurogranty. "
"Opieraj się na regulaminie konkretnego produktu."
),
"legal_references": [],
},
"NFOSIGW": {
"family": "NFOSIGW",
"instrument_kind": "environmental_investment",
"label": "NFOŚiGW — ochrona środowiska / efektywność energetyczna",
"funds_rnd": False,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Opis przedsięwzięcia i efekt ekologiczny",
"Wnioskodawca i kwalifikowalność",
"Zakres rzeczowy i techniczny",
"Efekt środowiskowy / energetyczny i wskaźniki",
"Budżet i źródła finansowania",
"Harmonogram",
"DNSH / zasady horyzontalne (jeśli wymagane)",
"Załączniki",
],
"allowed_costs": ["koszty kwalifikowalne wg naboru NFOŚiGW"],
"forbidden_costs": ["moduły SMART niezwiązane z naborem", "koszty niekwalifikowalne z regulaminu"],
"obligatory_criteria": [
"kwalifikowalność przedsięwzięcia środowiskowego",
"wymierny efekt ekologiczny/energetyczny",
],
"indicators": ["wskaźniki efektu ekologicznego / OZE / EE wg naboru"],
"required_company_fields": ["nip", "company_name"],
"required_project_fields": ["project_description"],
"allow_smart_modules": False,
"generation_rules": (
"Instrument NFOŚiGW — skup się na efekcie środowiskowym/energetycznym i kosztach "
"kwalifikowalnych naboru. NIE wstawiaj domyślnych modułów SMART B+R."
),
"legal_references": [],
},
"REGIONAL": {
"family": "REGIONAL",
"instrument_kind": "regional_fes",
"label": "Programy regionalne / FE / RPO",
"funds_rnd": False,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Informacje o projekcie i naborze regionalnym",
"Wnioskodawca i kwalifikowalność (region, status)",
"Opis projektu i uzasadnienie",
"Zgodność z celami RPO / FE i kryteriami naboru",
"Budżet i wkład własny",
"Harmonogram",
"Wskaźniki produktu i rezultatu",
"Zasady horyzontalne / DNSH (jeśli dotyczy)",
"Załączniki",
],
"allowed_costs": ["koszty kwalifikowalne wg regulaminu naboru regionalnego"],
"forbidden_costs": ["koszty poza katalogiem naboru", "ślepe kopiowanie modułów SMART"],
"obligatory_criteria": [
"lokalizacja / kwalifikowalność terytorialna",
"zgodność z typem projektu w naborze",
],
"indicators": ["wskaźniki z karty naboru / regulaminu"],
"required_company_fields": ["nip", "company_name", "msp_status"],
"required_project_fields": ["project_description"],
"allow_smart_modules": False,
"generation_rules": (
"Nabór regionalny (FE/RPO) — sekcje i koszty WYŁĄCZNIE z regulaminu tego naboru. "
"NIE zakładaj struktury Ścieżki SMART ani Eurograntów."
),
"legal_references": [],
},
# STEP (Strategic Technologies for Europe Platform) / dual-use — mid-term priority.
# Must NOT inherit Ścieżka SMART multi-module structure (B+R / Cyfryzacja / Zazielenienie…).
"STEP": {
"family": "STEP",
"instrument_kind": "critical_tech_dual_use",
"label": "STEP / dual-use — technologie krytyczne",
"funds_rnd": True,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Informacje o projekcie i naborze STEP",
"Wnioskodawca i kwalifikowalność",
"Opis technologii krytycznej / dual-use (jeśli dotyczy)",
"Innowacyjność i wkład w strategiczną niezależność UE",
"Zakres rzeczowy, plan prac i wdrożenie",
"Budżet i montaż finansowy (koszty kwalifikowalne)",
"Harmonogram",
"Wskaźniki produktu i rezultatu",
"DNSH / zasady horyzontalne / bezpieczeństwo (dual-use)",
"Załączniki i oświadczenia",
],
"allowed_costs": [
"koszty kwalifikowalne wg regulaminu naboru STEP / dual-use",
"prace B+R i wdrożenie technologii krytycznej (jeśli nabór dopuszcza)",
"infrastruktura niezbędna do realizacji projektu STEP",
],
"forbidden_costs": [
"moduły SMART (Cyfryzacja, Zazielenienie, Kompetencje, Internacjonalizacja) "
"jako domyślna struktura wniosku",
"ślepe kopiowanie szablonu Ścieżki SMART FENG.01",
"koszty poza katalogiem naboru STEP",
],
"obligatory_criteria": [
"zgodność z zakresem STEP (technologie cyfrowe / biotech / czyste technologie)",
"wkład w strategiczną niezależność UE lub dual-use (jeśli dotyczy)",
"kwalifikowalność wnioskodawcy wg naboru",
],
"indicators": ["wskaźniki produktu i rezultatu wg karty naboru STEP"],
"required_company_fields": [
"nip",
"company_name",
"pkd_codes",
"msp_status",
],
"required_project_fields": [
"project_description",
"critical_technology_area",
"innovation_description",
],
"allow_smart_modules": False,
"generation_rules": STEP_GENERATION_RULES,
"legal_references": [],
},
# Kredyt Ekologiczny (BGK / continuous) — debt + ecological premium; not SMART grant modules.
"KREDYT_EKO": {
"family": "KREDYT_EKO",
"instrument_kind": "green_credit",
"label": "Kredyt Ekologiczny — finansowanie dłużne proekologiczne",
"funds_rnd": False,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "loan",
"required_sections": [
"Informacje o kredycie / programie (BGK Kredyt Ekologiczny)",
"Wnioskodawca i zdolność kredytowa",
"Cel ekologiczny inwestycji i kwalifikowalność",
"Zakres rzeczowy i koszty kwalifikowalne",
"Montaż finansowy (kredyt + premia / wkład własny)",
"Harmonogram realizacji i spłaty",
"Efekty środowiskowe / wskaźniki EE / OZE",
"Zabezpieczenia i warunki bankowe",
"Załączniki i oświadczenia",
],
"allowed_costs": [
"inwestycje proekologiczne wg katalogu programu Kredyt Ekologiczny",
"modernizacja energetyczna / OZE (jeśli program dopuszcza)",
"koszty kwalifikowalne powiązane z celem ekologicznym",
],
"forbidden_costs": [
"moduły SMART (B+R, Cyfryzacja, Zazielenienie, Kompetencje) jako struktura wniosku grantowego",
"ślepe kopiowanie szablonu Ścieżki SMART FENG.01",
"koszty niekwalifikowalne wg regulaminu kredytu",
"traktowanie produktu jak czystej dotacji inwestycyjnej bez komponentu dłużnego",
],
"obligatory_criteria": [
"cel proekologiczny zgodny z programem",
"zdolność kredytowa / zabezpieczenia",
"zgodność kosztów z katalogiem Kredytu Ekologicznego",
],
"indicators": ["efekty środowiskowe / oszczędność energii / OZE wg programu"],
"required_company_fields": ["nip", "company_name", "msp_status"],
"required_project_fields": [
"project_description",
"ecological_objective",
"loan_amount_pln",
],
"allow_smart_modules": False,
"generation_rules": KREDYT_EKO_GENERATION_RULES,
"legal_references": [],
},
# Kredyt Technologiczny (BGK / continuous) — debt + tech premium; not SMART multi-modules.
"KREDYT_TECH": {
"family": "KREDYT_TECH",
"instrument_kind": "tech_credit",
"label": "Kredyt Technologiczny — finansowanie dłużne na innowacje technologiczne",
"funds_rnd": False,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "loan",
"required_sections": [
"Informacje o kredycie / programie (BGK Kredyt Technologiczny)",
"Wnioskodawca i zdolność kredytowa",
"Opis technologii / innowacji będącej przedmiotem finansowania",
"Zakres rzeczowy i koszty kwalifikowalne",
"Montaż finansowy (kredyt + premia technologiczna / wkład własny)",
"Harmonogram realizacji i spłaty",
"Efekty wdrożenia / wskaźniki technologiczne",
"Zabezpieczenia i warunki bankowe",
"Załączniki i oświadczenia",
],
"allowed_costs": [
"zakup i wdrożenie nowej technologii wg katalogu Kredytu Technologicznego",
"koszty kwalifikowalne powiązane z innowacją technologiczną (jeśli program dopuszcza)",
"szkolenia operatorów powiązane z zakupioną technologią (jeśli dopuszczone)",
],
"forbidden_costs": [
"moduły SMART (B+R, Cyfryzacja, Zazielenienie, Kompetencje) jako domyślna struktura wniosku",
"ślepe kopiowanie szablonu Ścieżki SMART FENG.01",
"koszty poza katalogiem regulaminu kredytu",
"traktowanie produktu jak pure grant B+R bez komponentu dłużnego",
],
"obligatory_criteria": [
"przedmiot = wdrożenie nowej technologii zgodne z programem",
"zdolność kredytowa / zabezpieczenia",
"zgodność kosztów z katalogiem Kredytu Technologicznego",
],
"indicators": ["wskaźniki wdrożenia technologii / premii technologicznej wg programu"],
"required_company_fields": ["nip", "company_name", "msp_status", "pkd_codes"],
"required_project_fields": [
"project_description",
"technology_description",
"loan_amount_pln",
],
"allow_smart_modules": False,
"generation_rules": KREDYT_TECH_GENERATION_RULES,
"legal_references": [],
},
# FST — Just Transition Fund regional track (higher intensity); not SMART multi-module.
"FST": {
"family": "FST",
"instrument_kind": "just_transition_regional",
"label": "Fundusz Sprawiedliwej Transformacji (FST) — nabór regionalny",
"funds_rnd": True,
"funds_infrastructure": True,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Informacje o naborze FST / operatorze regionalnym",
"Wnioskodawca i kwalifikowalność terytorialna FST",
"Opis projektu i wkład w transformację regionu",
"Zakres rzeczowy i plan prac",
"Budżet i koszty kwalifikowalne",
"Harmonogram",
"Wskaźniki produktu i rezultatu FST",
"DNSH / zasady horyzontalne",
"Załączniki",
],
"allowed_costs": ["koszty kwalifikowalne wg regulaminu naboru FST"],
"forbidden_costs": [
"domyślne moduły SMART jako struktura wniosku",
"koszty poza katalogiem naboru FST",
],
"obligatory_criteria": [
"lokalizacja w obszarze FST",
"zgodność z celami sprawiedliwej transformacji",
],
"indicators": ["wskaźniki FST wg karty naboru"],
"required_company_fields": ["nip", "company_name", "msp_status", "region"],
"required_project_fields": ["project_description"],
"allow_smart_modules": False,
"generation_rules": (
"Nabór FST — sekcje i koszty WYŁĄCZNIE z regulaminu operatora regionalnego. "
"NIE zakładaj struktury multi-modułowej Ścieżki SMART."
),
"legal_references": [],
},
# Startup Booster / First Team style PARP-NCBR tracks.
"FIRST_TEAM": {
"family": "FIRST_TEAM",
"instrument_kind": "startup_team_grant",
"label": "First Team / Startup Booster — zespół i komercjalizacja",
"funds_rnd": True,
"funds_infrastructure": False,
"funds_implementation": True,
"budget_form": "eligible_costs",
"required_sections": [
"Informacje o naborze i operatorze",
"Zespół / founderzy i kwalifikowalność",
"Opis rozwiązania i modelu biznesowego",
"Plan prac / milestones",
"Budżet",
"Harmonogram",
"Wskaźniki (traction / IP / sprzedaż)",
"Załączniki",
],
"allowed_costs": ["koszty kwalifikowalne wg regulaminu naboru"],
"forbidden_costs": [
"kopiowanie multi-modułów SMART bez podstawy w regulaminie",
],
"obligatory_criteria": ["kwalifikowalność zespołu / MŚP wg naboru"],
"indicators": ["wskaźniki naboru (traction, IP, zatrudnienie)"],
"required_company_fields": ["nip", "company_name", "msp_status"],
"required_project_fields": ["project_description", "innovation_description"],
"allow_smart_modules": False,
"generation_rules": (
"First Team / Startup Booster — NIE używaj struktury SMART multi-module. "
"Trzymaj się sekcji i kosztów z regulaminu naboru."
),
"legal_references": [],
},
# Direct EU Horizon / EIC prep schema (not SMART PL).
"HORIZON_PREP": {
"family": "HORIZON_PREP",
"instrument_kind": "direct_eu_prep",
"label": "Horizon Europe / EIC — przygotowanie wniosku (Direct EU)",
"funds_rnd": True,
"funds_infrastructure": False,
"funds_implementation": False,
"budget_form": "eligible_costs",
"required_sections": [
"Call / Work Programme / deadline",
"Excellence — cele i podejście",
"Impact — ścieżka wpływu",
"Implementation — work packages / konsorcjum",
"Budżet HE (jeśli wymagany na etapie prep)",
"TRL i ryzyko",
"Załączniki (concept note / short proposal)",
],
"allowed_costs": ["koszty przygotowawcze wg naboru / Eurograntu jeśli dotyczy"],
"forbidden_costs": [
"struktura Ścieżki SMART FENG.01 jako domyślny szablon HE",
"moduły Cyfryzacja/Zazielenienie SMART bez podstawy w callu UE",
],
"obligatory_criteria": ["zgodność z call / topic HE lub EIC"],
"indicators": ["TRL, impact KPIs wg callu"],
"required_company_fields": ["nip", "company_name"],
"required_project_fields": [
"project_description",
"eurogrant_programme",
"eurogrant_call",
],
"allow_smart_modules": False,
"generation_rules": (
"Direct EU / Horizon prep. NIGDY nie mapuj na multi-moduł SMART PL. "
"Użyj sekcji HE/EIC lub Eurogranty prep — nie FENG.01."
),
"legal_references": [],
},
}
def list_gold_families() -> List[str]:
"""Canonical gold family ids (excluding GENERIC empty fallback)."""
return sorted(k for k in _GOLD.keys() if k and k != "GENERIC")
def empty_schema(*, reason: str = "unknown") -> Dict[str, Any]:
return {
"version": SCHEMA_VERSION,
"family": "GENERIC",
"instrument_kind": "unknown",
"label": "Nieznany instrument",
"funds_rnd": False,
"funds_infrastructure": False,
"funds_implementation": False,
"budget_form": "unknown",
"required_sections": [],
"allowed_costs": [],
"forbidden_costs": [],
"obligatory_criteria": [],
"indicators": [],
"key_rules": [],
"required_company_fields": ["nip", "company_name"],
"required_project_fields": ["project_description"],
"allow_smart_modules": False,
"generation_rules": (
"Instrument nie w pełni rozpoznany. NIE zakładaj modułów SMART. "
"Opieraj się wyłącznie na regułach z regulaminu w kontekście. "
f"(reason={reason})"
),
"source": reason,
"confidence": 0.0,
}
def _merge_schema(base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]:
out = dict(base)
for k, v in (overlay or {}).items():
if v is None or v == "" or v == []:
continue
if isinstance(v, list) and isinstance(out.get(k), list):
# prefer non-empty overlay lists
out[k] = list(v) if v else out[k]
else:
out[k] = v
return out
_LEGAL_REF_PATTERNS = [
# Dz.U. / Dziennik Ustaw
re.compile(
r"(?:Dz\.\s*U\.|Dziennik\s+Ustaw)[^\n]{0,80}?(?:poz\.\s*\d+|\d{4}\s*(?:r\.?|rok)?[^\n]{0,40})",
re.IGNORECASE,
),
# CELEX
re.compile(r"\bCELEX[:\s]*[0-9A-Z]{5,20}\b", re.IGNORECASE),
# ELI / EUR-Lex URLs
re.compile(
r"https?://(?:eur-lex\.europa\.eu|data\.europa\.eu/eli)[^\s\)\"']{5,180}",
re.IGNORECASE,
),
# ISAP / sejm
re.compile(
r"https?://isap\.sejm\.gov\.pl[^\s\)\"']{5,180}",
re.IGNORECASE,
),
# art. / § with act hint
re.compile(
r"(?:art\.?\s*\d+[a-z]?(?:\s*ust\.?\s*\d+)?[^\n]{0,40}?(?:ustawy|rozporządzenia|regulaminu))",
re.IGNORECASE,
),
]
def extract_legal_references_from_text(text: str, *, limit: int = 15) -> List[Dict[str, str]]:
"""Pull legal act citations / ISAP / EUR-Lex URLs from regulation text."""
if not text or len(text.strip()) < 20:
return []
found: List[Dict[str, str]] = []
seen: set[str] = set()
for pat in _LEGAL_REF_PATTERNS:
for m in pat.finditer(text):
raw = re.sub(r"\s+", " ", m.group(0).strip())[:240]
key = raw.lower()
if len(raw) < 8 or key in seen:
continue
seen.add(key)
role = "citation"
low = raw.lower()
if "isap.sejm" in low:
role = "isap"
elif "eur-lex" in low or "eli/" in low or "celex" in low:
role = "eurlex"
elif "dz.u" in low or "dziennik" in low:
role = "dz_u"
entry: Dict[str, str] = {"title": raw, "role": role}
if raw.startswith("http"):
entry["url"] = raw
found.append(entry)
if len(found) >= limit:
return found
return found
def extract_instrument_hints_from_text(text: str) -> Dict[str, Any]:
"""Heuristic enrichment from regulation/announcement text."""
if not text or len(text.strip()) < 40:
return {}
t = text
low = t.lower()
out: Dict[str, Any] = {"key_rules": extract_key_rules_from_text(t)[:15]}
brief = build_advisor_brief_from_text(t)
if brief.get("required_sections"):
out["required_sections_extracted"] = list(brief["required_sections"])[:20]
if brief.get("required_attachments"):
out["required_attachments"] = list(brief["required_attachments"])[:15]
if brief.get("attention_points"):
out["attention_points"] = list(brief["attention_points"])[:10]
legal = extract_legal_references_from_text(t)
if legal:
out["legal_references"] = legal
if any(k in low for k in ("ryczałt", "ryczalt", "kwota ryczałtowa", "lump sum")):
out["budget_form"] = "lump_sum"
elif any(k in low for k in ("koszty kwalifikowalne", "refundac", "dofinansowanie kosztów")):
out["budget_form"] = "eligible_costs"
if any(k in low for k in ("eurogrant", "horyzont europa", "horizon europe", "digital europe")):
out["instrument_kind_hint"] = "preparatory_lump_sum"
if any(k in low for k in ("moduł b+r", "ścieżka smart", "sciezka smart", "trl")):
out["instrument_kind_hint"] = out.get("instrument_kind_hint") or "innovation_modules"
# Cost allow/forbid snippets
allowed: List[str] = []
forbidden: List[str] = []
for m in re.finditer(
r"koszty?\s+kwalifikowalne[^\n.]{0,120}",
t,
re.I,
):
allowed.append(m.group(0).strip()[:200])
for m in re.finditer(
r"(?:koszty?\s+niekwalifikowalne|nie\s+kwalifikuje\s+się)[^\n.]{0,120}",
t,
re.I,
):
forbidden.append(m.group(0).strip()[:200])
if allowed:
out["allowed_costs"] = allowed[:10]
if forbidden:
out["forbidden_costs"] = forbidden[:10]
return out
def build_instrument_schema(
*,
program_type: str = "",
program_name: str = "",
grant_id: str = "",
description: str = "",
regulation_text: str = "",
advisor_brief: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Build full instrument_schema for a project/nabor.
Never defaults to allow_smart_modules=True unless family is SMART.
"""
family = resolve_program_type(
program_type=program_type,
program_name=program_name,
grant_id=grant_id,
description=description or regulation_text[:1500],
)
gold = _GOLD.get(family)
if gold:
schema = {
"version": SCHEMA_VERSION,
"family": family,
"source": "gold_profile",
"confidence": 0.85,
**{k: v for k, v in gold.items()},
}
else:
schema = empty_schema(reason="no_gold_profile")
schema["family"] = family
# Pull light defaults from instrument_profile if any
prof = get_instrument_profile(family)
if prof.get("required_sections"):
schema["required_sections"] = list(prof["required_sections"])
schema["confidence"] = 0.45
schema["source"] = "instrument_profile"
if prof.get("generation_rules"):
schema["generation_rules"] = prof["generation_rules"]
schema["instrument_kind"] = prof.get("instrument_kind") or schema["instrument_kind"]
schema["label"] = prof.get("label") or family
# Text / brief enrichment
hints = extract_instrument_hints_from_text(regulation_text or "")
if hints.get("key_rules"):
schema["key_rules"] = hints["key_rules"]
if hints.get("budget_form"):
schema["budget_form"] = hints["budget_form"]
if hints.get("allowed_costs") and not schema.get("allowed_costs"):
schema["allowed_costs"] = hints["allowed_costs"]
if hints.get("forbidden_costs"):
existing = list(schema.get("forbidden_costs") or [])
schema["forbidden_costs"] = (existing + list(hints["forbidden_costs"]))[:15]
if hints.get("legal_references"):
existing_legal = list(schema.get("legal_references") or [])
# merge by title/url
seen_l = {
(str(x.get("url") or x.get("title") or "")).lower()
for x in existing_legal
if isinstance(x, dict)
}
for ref in hints["legal_references"]:
if not isinstance(ref, dict):
continue
key = str(ref.get("url") or ref.get("title") or "").lower()
if key and key not in seen_l:
existing_legal.append(ref)
seen_l.add(key)
schema["legal_references"] = existing_legal[:15]
# Prefer gold/profile sections; if empty, use extracted headers (still no SMART invent)
if not schema.get("required_sections") and hints.get("required_sections_extracted"):
schema["required_sections"] = hints["required_sections_extracted"]
schema["confidence"] = max(float(schema.get("confidence") or 0), 0.4)
schema["source"] = f"{schema.get('source')}+text_sections"
if advisor_brief and isinstance(advisor_brief, dict):
if advisor_brief.get("required_sections") and not schema.get("required_sections"):
schema["required_sections"] = list(advisor_brief["required_sections"])[:20]
if advisor_brief.get("key_rules"):
schema["key_rules"] = list(
dict.fromkeys(
list(schema.get("key_rules") or [])
+ list(advisor_brief.get("key_rules") or [])
)
)[:20]
if advisor_brief.get("attention_points"):
schema["attention_points"] = list(advisor_brief["attention_points"])[:10]
# Hard safety: only SMART family may use smart modules
if family != "SMART":
schema["allow_smart_modules"] = False
else:
schema["allow_smart_modules"] = True
schema["family"] = family
schema["version"] = SCHEMA_VERSION
schema["program_name"] = program_name or ""
schema["grant_id"] = grant_id or ""
return schema
def schema_seed_sections(schema: Optional[Dict[str, Any]]) -> List[str]:
"""Section titles to seed for a project."""
if not schema:
return []
return [str(s).strip() for s in (schema.get("required_sections") or []) if str(s).strip()]
def schema_generation_prompt(schema: Optional[Dict[str, Any]]) -> str:
if not schema:
return ""
rules = (schema.get("generation_rules") or "").strip()
lines = [
f"=== INSTRUMENT SCHEMA v{schema.get('version')} | family={schema.get('family')} "
f"| kind={schema.get('instrument_kind')} | budget={schema.get('budget_form')} ===",
f"Label: {schema.get('label')}",
]
if schema.get("allowed_costs"):
lines.append("Koszty dopuszczalne (kierunkowo):")
lines.extend(f"- {c}" for c in schema["allowed_costs"][:12])
if schema.get("forbidden_costs"):
lines.append("ZAKAZANE / nie finansowane:")
lines.extend(f"- {c}" for c in schema["forbidden_costs"][:12])
if schema.get("obligatory_criteria"):
lines.append("Kryteria obligatoryjne:")
lines.extend(f"- {c}" for c in schema["obligatory_criteria"][:10])
if schema.get("indicators"):
lines.append("Wskaźniki oczekiwane:")
lines.extend(f"- {c}" for c in schema["indicators"][:8])
if rules:
lines.append(rules)
if not schema.get("allow_smart_modules"):
lines.append(
"ZAKAZ: nie twórz modułów SMART (B+R, Wdrożenie, Infrastruktura B+R, "
"Cyfryzacja, Zazielenienie, Kompetencje), chyba że wprost w required_sections."
)
return "\n".join(lines)
def schema_allows_smart_fallback(schema: Optional[Dict[str, Any]]) -> bool:
"""True only for SMART family with explicit allow."""
if not schema:
return False
return bool(schema.get("allow_smart_modules")) and schema.get("family") == "SMART"