|
"""A Flask-based front-end for the Sustainable Finance Hub. |
|
|
|
This module provides an alternative to the Streamlit interface using |
|
the Flask web framework. It avoids Streamlit’s heavy dependency on |
|
PyArrow, making installation simpler in environments where compiling |
|
native extensions is problematic. The Flask version preserves the |
|
core functionality: language selection, AI consultation via Groq, |
|
management of financial providers, searching the provider database, |
|
project submission with recommendations, and a page about the GXS |
|
Network. Data are persisted via the ``SustainableFinanceHub`` class. |
|
|
|
Usage: |
|
|
|
1. Install dependencies: ``pip install flask requests pillow`` |
|
2. (Optionally) set ``GROQ_API_KEY`` in your environment. |
|
3. Run: ``python sustainable_finance_hub_flask.py`` |
|
4. Open ``http://127.0.0.1:5000`` in your web browser. |
|
|
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
import json |
|
import os |
|
from typing import Dict, List |
|
from urllib.parse import urlencode |
|
from flask import Flask, request, redirect, url_for |
|
|
|
import requests |
|
|
|
|
|
import sys |
|
sys.path.append(os.path.dirname(os.path.abspath(__file__))) |
|
|
|
from sustainable_finance_hub import ( |
|
SustainableFinanceHub, |
|
FinancialProvider, |
|
ProjectProposal, |
|
) |
|
|
|
|
|
app = Flask(__name__) |
|
|
|
hub = SustainableFinanceHub() |
|
|
|
|
|
def call_groq_chat(api_key: str, messages: List[Dict[str, str]], model: str = "llama-3.3-70b-versatile", temperature: float = 0.7, max_tokens: int = 512) -> str: |
|
url = "https://api.groq.com/openai/v1/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {api_key}", |
|
"Content-Type": "application/json", |
|
} |
|
payload = { |
|
"model": model, |
|
"messages": messages, |
|
"temperature": temperature, |
|
"max_tokens": max_tokens, |
|
} |
|
try: |
|
response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60) |
|
except Exception as exc: |
|
return f"Error contacting Groq API: {exc}" |
|
if response.status_code != 200: |
|
return f"Groq API returned status {response.status_code}: {response.text}" |
|
try: |
|
res_json = response.json() |
|
return res_json["choices"][0]["message"]["content"] |
|
except Exception: |
|
return f"Unexpected response from Groq API: {response.text}" |
|
|
|
|
|
def translate_text_to_vietnamese(text: str, api_key: str) -> str: |
|
if not api_key: |
|
return text + "\n\n[Chú ý: Không có khóa API để dịch. Nội dung vẫn bằng tiếng Anh.]" |
|
messages = [ |
|
{"role": "system", "content": "Bạn là một dịch giả chuyên nghiệp từ tiếng Anh sang tiếng Việt."}, |
|
{"role": "user", "content": f"Hãy dịch đoạn văn sau sang tiếng Việt:\n\n{text}"}, |
|
] |
|
return call_groq_chat(api_key, messages) |
|
|
|
|
|
UI_TRANSLATIONS: Dict[str, Dict[str, str]] = { |
|
"home_title": {"vi": "Trung Tâm Tài Chính Bền Vững"}, |
|
"home_welcome": {"vi": "Chào mừng đến với Trung tâm Tài chính Bền vững!"}, |
|
"nav_home": {"vi": "Trang chính"}, |
|
"nav_consult": {"vi": "Tư vấn AI"}, |
|
"nav_add_provider": {"vi": "Thêm nhà cung cấp"}, |
|
"nav_search": {"vi": "Tìm nhà cung cấp"}, |
|
"nav_add_project": {"vi": "Thêm dự án & Gợi ý"}, |
|
"nav_gxs": {"vi": "Mạng lưới GXS"}, |
|
"select_language": {"vi": "Chọn ngôn ngữ"}, |
|
|
|
"consult_title": {"vi": "Tư vấn AI"}, |
|
"consult_prompt": {"vi": "Đặt câu hỏi của bạn về hệ thống phân loại xanh, tài chính xanh/khí hậu, tài chính bền vững và đầu tư tại Đông Nam Á."}, |
|
"consult_api_key": {"vi": "Khóa API Groq"}, |
|
"consult_question_placeholder": {"vi": "Nhập câu hỏi của bạn ở đây"}, |
|
"consult_submit": {"vi": "Gửi câu hỏi"}, |
|
"consult_answer": {"vi": "Phản hồi của AI"}, |
|
"missing_api_key": {"vi": "Vui lòng nhập khóa API."}, |
|
|
|
"consult_environment_msg": {"vi": "Khóa API Groq đã được tải từ biến môi trường và sẽ được sử dụng tự động."}, |
|
|
|
"provider_title": {"vi": "Thêm nhà cung cấp"}, |
|
"provider_name": {"vi": "Tên nhà cung cấp"}, |
|
"provider_type": {"vi": "Loại nhà cung cấp"}, |
|
"provider_description": {"vi": "Mô tả"}, |
|
"provider_funds": {"vi": "Quỹ sẵn có"}, |
|
"provider_sectors": {"vi": "Ngành (dấu phẩy)"}, |
|
"provider_location": {"vi": "Địa điểm"}, |
|
"provider_languages": {"vi": "Ngôn ngữ (mã, dấu phẩy)"}, |
|
"provider_add_button": {"vi": "Thêm"}, |
|
"provider_success": {"vi": "Đã thêm nhà cung cấp!"}, |
|
|
|
"search_title": {"vi": "Tìm nhà cung cấp"}, |
|
"search_sector": {"vi": "Ngành"}, |
|
"search_location": {"vi": "Địa điểm"}, |
|
"search_button": {"vi": "Tìm kiếm"}, |
|
"search_no_results": {"vi": "Không tìm thấy kết quả."}, |
|
|
|
"project_title": {"vi": "Thêm dự án và nhận gợi ý"}, |
|
"project_name": {"vi": "Tên dự án"}, |
|
"project_description": {"vi": "Mô tả dự án"}, |
|
"project_sector": {"vi": "Ngành"}, |
|
"project_location": {"vi": "Địa điểm"}, |
|
"project_funding": {"vi": "Vốn cần thiết"}, |
|
"project_languages": {"vi": "Ngôn ngữ (mã, dấu phẩy)"}, |
|
"project_submit": {"vi": "Gửi dự án"}, |
|
"project_success": {"vi": "Đã gửi dự án!"}, |
|
"recommendations_title": {"vi": "Gợi ý nhà cung cấp"}, |
|
|
|
"gxs_title": {"vi": "Mạng lưới GXS"}, |
|
"gxs_about": {"vi": "Giới thiệu"}, |
|
"gxs_question": {"vi": "Đặt câu hỏi"}, |
|
"gxs_submit": {"vi": "Nhận câu trả lời"}, |
|
"gxs_answer": {"vi": "Phản hồi"}, |
|
} |
|
|
|
|
|
def ui_text(key: str, lang: str) -> str: |
|
return UI_TRANSLATIONS.get(key, {}).get(lang, key) |
|
|
|
|
|
|
|
GXS_MISSION_EN = ( |
|
"Green Transformation and Sustainability Network (GXS) is a pioneering organisation driving the green economy, " |
|
"circular economy, biodiversity conservation, energy transition, social impact business, climate resilience, " |
|
"climate solutions, technology, governance, investment, sustainable development and green innovation across Vietnam " |
|
"and Southeast Asia. The GXS Network focuses on empowering local businesses and communities, building collaborative " |
|
"networks for sustainable living and development. Its primary activities include promoting green economy initiatives, " |
|
"fostering circular economy practices, and implementing climate and sustainable solutions that align with the Vietnamese " |
|
"government and international commitments." |
|
) |
|
|
|
GXS_MISSION_VI = ( |
|
"Mạng lưới Chuyển đổi Xanh và Phát triển Bền vững (GXS) là một tổ chức tiên phong thúc đẩy kinh tế xanh, kinh tế tuần " |
|
"hoàn, bảo tồn đa dạng sinh học, chuyển dịch năng lượng, doanh nghiệp tác động xã hội, khả năng thích ứng khí hậu, " |
|
"các giải pháp khí hậu, công nghệ, quản trị, đầu tư, phát triển bền vững và đổi mới xanh trên khắp Việt Nam và Đông Nam Á. " |
|
"Mạng lưới GXS tập trung vào việc trao quyền cho các doanh nghiệp và cộng đồng địa phương, xây dựng các mạng lưới hợp tác " |
|
"để sống và phát triển bền vững. Các hoạt động chính bao gồm thúc đẩy các sáng kiến kinh tế xanh, nuôi dưỡng thực hành " |
|
"kinh tế tuần hoàn và triển khai các giải pháp khí hậu và bền vững phù hợp với các cam kết của Chính phủ Việt Nam và " |
|
"quốc tế." |
|
) |
|
|
|
|
|
def build_nav(lang: str) -> str: |
|
"""Return HTML for navigation bar with links preserving language.""" |
|
links = [ |
|
("/", ui_text("nav_home", lang)), |
|
("/consultation", ui_text("nav_consult", lang)), |
|
("/add_provider", ui_text("nav_add_provider", lang)), |
|
("/search_providers", ui_text("nav_search", lang)), |
|
("/add_project", ui_text("nav_add_project", lang)), |
|
("/gxs", ui_text("nav_gxs", lang)), |
|
] |
|
nav_items = [] |
|
for url, label in links: |
|
nav_items.append(f'<a href="{url}?lang={lang}">{label}</a>') |
|
return " | ".join(nav_items) |
|
|
|
|
|
@app.route("/") |
|
def home(): |
|
lang = request.args.get("lang", "en") |
|
title = ui_text("home_title", lang) |
|
nav = build_nav(lang) |
|
html = f""" |
|
<html><head><title>{title}</title></head><body> |
|
<h1>{title}</h1> |
|
<p>{ui_text('home_welcome', lang)}</p> |
|
<p>{ui_text('select_language', lang)}: |
|
<a href="/?lang=en">English</a> | |
|
<a href="/?lang=vi">Tiếng Việt</a> |
|
</p> |
|
<p>{nav}</p> |
|
</body></html> |
|
""" |
|
return html |
|
|
|
|
|
@app.route("/consultation", methods=["GET", "POST"]) |
|
def consultation(): |
|
lang = request.args.get("lang", "en") |
|
nav = build_nav(lang) |
|
answer = None |
|
error = None |
|
question = "" |
|
api_key = "" |
|
if request.method == "POST": |
|
question = request.form.get("question", "") |
|
api_key = request.form.get("api_key", "").strip() or os.environ.get("GROQ_API_KEY", "") |
|
if not api_key: |
|
error = ui_text("missing_api_key", lang) |
|
else: |
|
system_prompt = ( |
|
"You are an expert in green taxonomies, climate finance, sustainable finance and investing, focusing on Southeast Asia and Vietnam. Answer concisely." |
|
if lang == "en" else |
|
"Bạn là chuyên gia về hệ thống phân loại xanh, tài chính khí hậu, tài chính bền vững và đầu tư, tập trung vào Đông Nam Á và Việt Nam. Trả lời ngắn gọn bằng tiếng Việt." |
|
) |
|
messages = [ |
|
{"role": "system", "content": system_prompt}, |
|
{"role": "user", "content": question}, |
|
] |
|
answer = call_groq_chat(api_key, messages) |
|
|
|
|
|
html = f""" |
|
<html><head><title>{ui_text('consult_title', lang)}</title></head><body> |
|
<h1>{ui_text('consult_title', lang)}</h1> |
|
<p>{nav}</p> |
|
<form method="post"> |
|
<label>{ui_text('consult_prompt', lang)}</label><br> |
|
<textarea name="question" rows="6" cols="60" placeholder="{ui_text('consult_question_placeholder', lang)}">{question}</textarea><br> |
|
""" |
|
|
|
if not os.environ.get("GROQ_API_KEY", ""): |
|
html += f"<label>{ui_text('consult_api_key', lang)}:</label><br><input type='password' name='api_key'><br>" |
|
else: |
|
|
|
html += f"<p>{ui_text('consult_environment_msg', lang)}</p>" |
|
html += f"""<input type="submit" value="{ui_text('consult_submit', lang)}"> |
|
</form> |
|
""" |
|
if error: |
|
html += f"<p style='color:red'>{error}</p>" |
|
if answer: |
|
html += f"<h2>{ui_text('consult_answer', lang)}</h2><p>{answer}</p>" |
|
html += "</body></html>" |
|
return html |
|
|
|
|
|
@app.route("/add_provider", methods=["GET", "POST"]) |
|
def add_provider(): |
|
lang = request.args.get("lang", "en") |
|
nav = build_nav(lang) |
|
message = None |
|
if request.method == "POST": |
|
name = request.form.get("name", "") |
|
ptype = request.form.get("ptype", "") |
|
desc = request.form.get("desc", "") |
|
funds_str = request.form.get("funds", "0") |
|
sectors_str = request.form.get("sectors", "") |
|
loc = request.form.get("loc", "") |
|
langs_str = request.form.get("langs", "en") |
|
try: |
|
funds = float(funds_str.strip() or 0) |
|
except ValueError: |
|
funds = 0.0 |
|
sectors = [s.strip() for s in sectors_str.split(",") if s.strip()] |
|
langs = [l.strip() for l in langs_str.split(",") if l.strip()] |
|
provider = FinancialProvider( |
|
name=name.strip(), |
|
provider_type=ptype.strip(), |
|
description=desc.strip(), |
|
available_funds=funds, |
|
sectors=sectors, |
|
location=loc.strip(), |
|
languages=langs or ["en"], |
|
) |
|
hub.add_provider(provider) |
|
message = ui_text("provider_success", lang) |
|
html = f""" |
|
<html><head><title>{ui_text('provider_title', lang)}</title></head><body> |
|
<h1>{ui_text('provider_title', lang)}</h1> |
|
<p>{nav}</p> |
|
<form method="post"> |
|
<label>{ui_text('provider_name', lang)}</label><br> |
|
<input type="text" name="name"><br> |
|
<label>{ui_text('provider_type', lang)}</label><br> |
|
<input type="text" name="ptype"><br> |
|
<label>{ui_text('provider_description', lang)}</label><br> |
|
<textarea name="desc" rows="4" cols="60"></textarea><br> |
|
<label>{ui_text('provider_funds', lang)}</label><br> |
|
<input type="text" name="funds" value="0"><br> |
|
<label>{ui_text('provider_sectors', lang)}</label><br> |
|
<input type="text" name="sectors"><br> |
|
<label>{ui_text('provider_location', lang)}</label><br> |
|
<input type="text" name="loc"><br> |
|
<label>{ui_text('provider_languages', lang)}</label><br> |
|
<input type="text" name="langs" value="en"><br> |
|
<input type="submit" value="{ui_text('provider_add_button', lang)}"> |
|
</form> |
|
""" |
|
if message: |
|
html += f"<p style='color:green'>{message}</p>" |
|
html += "</body></html>" |
|
return html |
|
|
|
|
|
@app.route("/search_providers", methods=["GET", "POST"]) |
|
def search_providers(): |
|
lang = request.args.get("lang", "en") |
|
nav = build_nav(lang) |
|
results = [] |
|
sector_filter = "" |
|
location_filter = "" |
|
if request.method == "POST": |
|
sector_filter = request.form.get("sector", "").strip() |
|
location_filter = request.form.get("location", "").strip() |
|
results = hub.search_providers(sector_filter, location_filter) |
|
html = f""" |
|
<html><head><title>{ui_text('search_title', lang)}</title></head><body> |
|
<h1>{ui_text('search_title', lang)}</h1> |
|
<p>{nav}</p> |
|
<form method="post"> |
|
<label>{ui_text('search_sector', lang)}</label><br> |
|
<input type="text" name="sector" value="{sector_filter}"><br> |
|
<label>{ui_text('search_location', lang)}</label><br> |
|
<input type="text" name="location" value="{location_filter}"><br> |
|
<input type="submit" value="{ui_text('search_button', lang)}"> |
|
</form> |
|
""" |
|
if results: |
|
html += f"<h2>{ui_text('search_title', lang)}</h2>" |
|
for p in results: |
|
html += f"<p><strong>{p.name}</strong> ({p.provider_type})<br>" |
|
html += f"{p.description}<br>" |
|
html += f"{ui_text('provider_funds', lang)}: {p.available_funds}<br>" |
|
html += f"{ui_text('provider_sectors', lang)}: {', '.join(p.sectors)}<br>" |
|
html += f"{ui_text('provider_location', lang)}: {p.location}</p><hr>" |
|
elif request.method == "POST": |
|
html += f"<p>{ui_text('search_no_results', lang)}</p>" |
|
html += "</body></html>" |
|
return html |
|
|
|
|
|
@app.route("/add_project", methods=["GET", "POST"]) |
|
def add_project(): |
|
lang = request.args.get("lang", "en") |
|
nav = build_nav(lang) |
|
message = None |
|
recommendations = [] |
|
if request.method == "POST": |
|
name = request.form.get("name", "") |
|
desc = request.form.get("desc", "") |
|
sector = request.form.get("sector", "") |
|
loc = request.form.get("loc", "") |
|
funding_str = request.form.get("funding", "0") |
|
langs_str = request.form.get("langs", "en") |
|
try: |
|
funding = float(funding_str.strip() or 0) |
|
except ValueError: |
|
funding = 0.0 |
|
langs = [l.strip() for l in langs_str.split(",") if l.strip()] |
|
project = ProjectProposal( |
|
name=name.strip(), |
|
description=desc.strip(), |
|
sector=sector.strip(), |
|
location=loc.strip(), |
|
funding_needed=funding, |
|
languages=langs or ["en"], |
|
) |
|
hub.add_project(project) |
|
message = ui_text("project_success", lang) |
|
recommendations = hub.recommend_providers(project) |
|
html = f""" |
|
<html><head><title>{ui_text('project_title', lang)}</title></head><body> |
|
<h1>{ui_text('project_title', lang)}</h1> |
|
<p>{nav}</p> |
|
<form method="post"> |
|
<label>{ui_text('project_name', lang)}</label><br> |
|
<input type="text" name="name"><br> |
|
<label>{ui_text('project_description', lang)}</label><br> |
|
<textarea name="desc" rows="4" cols="60"></textarea><br> |
|
<label>{ui_text('project_sector', lang)}</label><br> |
|
<input type="text" name="sector"><br> |
|
<label>{ui_text('project_location', lang)}</label><br> |
|
<input type="text" name="loc"><br> |
|
<label>{ui_text('project_funding', lang)}</label><br> |
|
<input type="text" name="funding" value="0"><br> |
|
<label>{ui_text('project_languages', lang)}</label><br> |
|
<input type="text" name="langs" value="en"><br> |
|
<input type="submit" value="{ui_text('project_submit', lang)}"> |
|
</form> |
|
""" |
|
if message: |
|
html += f"<p style='color:green'>{message}</p>" |
|
if recommendations: |
|
html += f"<h2>{ui_text('recommendations_title', lang)}</h2>" |
|
for p in recommendations: |
|
html += f"<p><strong>{p.name}</strong> ({p.provider_type})<br>" |
|
html += f"{p.description}<br>" |
|
html += f"{ui_text('provider_funds', lang)}: {p.available_funds}<br>" |
|
html += f"{ui_text('provider_sectors', lang)}: {', '.join(p.sectors)}<br>" |
|
html += f"{ui_text('provider_location', lang)}: {p.location}</p><hr>" |
|
html += "</body></html>" |
|
return html |
|
|
|
|
|
@app.route("/gxs", methods=["GET", "POST"]) |
|
def gxs(): |
|
lang = request.args.get("lang", "en") |
|
nav = build_nav(lang) |
|
answer = None |
|
error = None |
|
question = "" |
|
if request.method == "POST": |
|
question = request.form.get("question", "") |
|
api_key = request.form.get("api_key", "").strip() or os.environ.get("GROQ_API_KEY", "") |
|
if not api_key: |
|
error = ui_text("missing_api_key", lang) |
|
else: |
|
system_prompt = ( |
|
"You are an expert on the Green Transformation and Sustainability (GXS) Network. Answer user questions about GXS programmes, activities and mission in Vietnam and Southeast Asia." |
|
if lang == "en" else |
|
"Bạn là chuyên gia về Mạng lưới Chuyển đổi Xanh và Phát triển Bền vững (GXS). Hãy trả lời câu hỏi của người dùng về các chương trình, hoạt động và sứ mệnh của GXS tại Việt Nam và Đông Nam Á bằng tiếng Việt." |
|
) |
|
messages = [ |
|
{"role": "system", "content": system_prompt}, |
|
{"role": "user", "content": question}, |
|
] |
|
answer = call_groq_chat(api_key, messages) |
|
mission = GXS_MISSION_EN if lang == "en" else GXS_MISSION_VI |
|
html = f""" |
|
<html><head><title>{ui_text('gxs_title', lang)}</title></head><body> |
|
<h1>{ui_text('gxs_title', lang)}</h1> |
|
<p>{nav}</p> |
|
<h2>{ui_text('gxs_about', lang)}</h2> |
|
<p>{mission}</p> |
|
<h2>{ui_text('gxs_question', lang)}</h2> |
|
<form method="post"> |
|
<textarea name="question" rows="4" cols="60" placeholder="{ui_text('gxs_question', lang)}">{question}</textarea><br> |
|
""" |
|
|
|
if not os.environ.get("GROQ_API_KEY", ""): |
|
html += f"<label>{ui_text('consult_api_key', lang)}:</label><br><input type='password' name='api_key'><br>" |
|
else: |
|
|
|
html += f"<p>{ui_text('consult_environment_msg', lang)}</p>" |
|
html += f"<input type="submit" value="{ui_text('gxs_submit', lang)}"></form>" |
|
if error: |
|
html += f"<p style='color:red'>{error}</p>" |
|
if answer: |
|
html += f"<h3>{ui_text('gxs_answer', lang)}</h3><p>{answer}</p>" |
|
html += "</body></html>" |
|
return html |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
app.run(debug=False) |