|
"""Core logic for the AI‑driven Sustainable Finance Hub. |
|
|
|
This module defines data structures and helper functions to manage |
|
financial providers and project proposals for green and sustainable |
|
finance in Southeast Asia and Vietnam. It also includes simple |
|
matching logic to recommend providers based on project needs. Data |
|
are persisted to JSON files in the local filesystem to keep the |
|
application state across sessions. |
|
|
|
The design emphasises simplicity and readability: it avoids external |
|
dependencies beyond Python’s standard library, enabling easy |
|
modification and debugging. Translation of interface strings and |
|
content is handled by the GUI module via Groq’s API (see |
|
``sustainable_finance_hub_gui.py``). |
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
import json |
|
import os |
|
from dataclasses import dataclass, asdict, field |
|
from typing import List, Dict, Optional |
|
|
|
|
|
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") |
|
|
|
|
|
def ensure_data_dir() -> None: |
|
"""Ensure the data directory exists.""" |
|
if not os.path.exists(DATA_DIR): |
|
os.makedirs(DATA_DIR, exist_ok=True) |
|
|
|
|
|
@dataclass |
|
class FinancialProvider: |
|
"""Represent a financial provider entity (e.g., bank, fund, VC). |
|
|
|
Attributes |
|
---------- |
|
name : str |
|
The provider's name. |
|
provider_type : str |
|
Type of provider (bank, fund, VC, etc.). |
|
description : str |
|
A brief description of the provider and its focus areas. |
|
available_funds : float |
|
The amount of funds available for investment in USD (or local currency). |
|
sectors : List[str] |
|
List of sectors the provider is interested in (e.g., renewable energy, climate tech). |
|
location : str |
|
Primary location or region served (e.g., Vietnam, Southeast Asia). |
|
languages : List[str] |
|
Languages the provider can operate in ("en", "vi", etc.). |
|
""" |
|
|
|
name: str |
|
provider_type: str |
|
description: str |
|
available_funds: float |
|
sectors: List[str] |
|
location: str |
|
languages: List[str] = field(default_factory=lambda: ["en"]) |
|
|
|
|
|
@dataclass |
|
class ProjectProposal: |
|
"""Represent a sustainable project proposal or SME seeking funding. |
|
|
|
Attributes |
|
---------- |
|
name : str |
|
Name of the project or business. |
|
description : str |
|
Summary of the project and its goals. |
|
sector : str |
|
Primary sector of the project (e.g., renewable energy). |
|
location : str |
|
Project location (e.g., Vietnam). |
|
funding_needed : float |
|
Amount of funding required in USD (or local currency). |
|
languages : List[str] |
|
Languages the project can operate in ("en", "vi", etc.). |
|
""" |
|
|
|
name: str |
|
description: str |
|
sector: str |
|
location: str |
|
funding_needed: float |
|
languages: List[str] = field(default_factory=lambda: ["en"]) |
|
|
|
|
|
class SustainableFinanceHub: |
|
"""Core class managing providers and project proposals. |
|
|
|
This class handles reading and writing provider and project data |
|
from JSON files. It also implements a simple recommendation |
|
algorithm that matches project proposals to providers based on |
|
shared sectors and geographic alignment. The matching is naive |
|
but sufficient for prototype purposes; more sophisticated logic |
|
could be integrated later (e.g., weighting by fund size or using |
|
semantic similarity). |
|
""" |
|
|
|
def __init__(self) -> None: |
|
ensure_data_dir() |
|
self.providers_file = os.path.join(DATA_DIR, "providers.json") |
|
self.projects_file = os.path.join(DATA_DIR, "projects.json") |
|
self.providers: List[FinancialProvider] = [] |
|
self.projects: List[ProjectProposal] = [] |
|
self.load_providers() |
|
self.load_projects() |
|
|
|
|
|
def load_providers(self) -> None: |
|
"""Load providers from the JSON file into memory.""" |
|
if os.path.exists(self.providers_file): |
|
try: |
|
with open(self.providers_file, "r", encoding="utf-8") as f: |
|
data = json.load(f) |
|
self.providers = [FinancialProvider(**p) for p in data] |
|
except Exception: |
|
self.providers = [] |
|
else: |
|
self.providers = [] |
|
|
|
def save_providers(self) -> None: |
|
"""Write provider list to the JSON file.""" |
|
with open(self.providers_file, "w", encoding="utf-8") as f: |
|
json.dump([asdict(p) for p in self.providers], f, ensure_ascii=False, indent=2) |
|
|
|
def load_projects(self) -> None: |
|
"""Load project proposals from the JSON file into memory.""" |
|
if os.path.exists(self.projects_file): |
|
try: |
|
with open(self.projects_file, "r", encoding="utf-8") as f: |
|
data = json.load(f) |
|
self.projects = [ProjectProposal(**p) for p in data] |
|
except Exception: |
|
self.projects = [] |
|
else: |
|
self.projects = [] |
|
|
|
def save_projects(self) -> None: |
|
"""Write project list to the JSON file.""" |
|
with open(self.projects_file, "w", encoding="utf-8") as f: |
|
json.dump([asdict(p) for p in self.projects], f, ensure_ascii=False, indent=2) |
|
|
|
|
|
def add_provider(self, provider: FinancialProvider) -> None: |
|
"""Add a financial provider to the database and save.""" |
|
self.providers.append(provider) |
|
self.save_providers() |
|
|
|
def add_project(self, project: ProjectProposal) -> None: |
|
"""Add a project proposal to the database and save.""" |
|
self.projects.append(project) |
|
self.save_projects() |
|
|
|
|
|
def search_providers(self, sector: Optional[str] = None, location: Optional[str] = None) -> List[FinancialProvider]: |
|
"""Return providers matching the given sector and location. |
|
|
|
Parameters |
|
---------- |
|
sector : Optional[str] |
|
Sector filter; if None, all sectors are matched. |
|
location : Optional[str] |
|
Location filter; if None, all locations are matched. |
|
|
|
Returns |
|
------- |
|
List[FinancialProvider] |
|
Providers that match the given criteria. |
|
""" |
|
results = [] |
|
for p in self.providers: |
|
sector_match = True if (sector is None or sector.strip() == "") else (sector.lower() in [s.lower() for s in p.sectors]) |
|
location_match = True if (location is None or location.strip() == "") else (location.lower() in p.location.lower()) |
|
if sector_match and location_match: |
|
results.append(p) |
|
return results |
|
|
|
def recommend_providers(self, project: ProjectProposal, top_n: int = 5) -> List[FinancialProvider]: |
|
"""Recommend providers for a given project based on sector and location. |
|
|
|
The recommendation score is computed as the number of matching |
|
sectors plus 1 if locations overlap (case-insensitive substring). |
|
Providers are sorted by score (descending) and by available funds |
|
(descending) to prioritise larger funders. Only the top `top_n` |
|
providers are returned. |
|
|
|
Parameters |
|
---------- |
|
project : ProjectProposal |
|
The project for which to find funding matches. |
|
top_n : int |
|
Number of top providers to return. |
|
|
|
Returns |
|
------- |
|
List[FinancialProvider] |
|
The recommended providers. |
|
""" |
|
scored: List[tuple[FinancialProvider, int]] = [] |
|
for p in self.providers: |
|
|
|
sector_overlap = sum(1 for s in p.sectors if s.lower() == project.sector.lower()) |
|
if sector_overlap == 0: |
|
continue |
|
location_score = 1 if project.location.lower() in p.location.lower() else 0 |
|
score = sector_overlap + location_score |
|
scored.append((p, score)) |
|
|
|
scored.sort(key=lambda x: (x[1], x[0].available_funds), reverse=True) |
|
return [p for p, _ in scored[:top_n]] |