Spaces:
Running
Running
| """ | |
| 匯率查詢 MCP Tool | |
| 使用標準化接口實作的匯率功能 | |
| """ | |
| import os | |
| import json | |
| import logging | |
| import aiohttp | |
| import asyncio | |
| from datetime import datetime | |
| from typing import Dict, Any, Optional | |
| from dotenv import load_dotenv | |
| from .base_tool import MCPTool, ValidationError, ExecutionError, StandardToolSchemas | |
| # 載入環境變數 | |
| load_dotenv() | |
| logger = logging.getLogger("mcp.tools.exchange") | |
| # API配置 | |
| EXCHANGE_API_BASE = "https://api.exchangerate-api.com/v4/latest" | |
| FIXER_API_BASE = "https://data.fixer.io/api/latest" | |
| FIXER_API_KEY = os.getenv("FIXER_API_KEY", "") | |
| class ExchangeTool(MCPTool): | |
| """匯率查詢 MCP 工具""" | |
| NAME = "exchange_query" | |
| DESCRIPTION = "Query real-time exchange rates between major currencies" | |
| CATEGORY = "生活資訊" | |
| TAGS = ["exchange", "匯率", "貨幣"] | |
| KEYWORDS = ["匯率", "美元", "台幣", "exchange", "USD", "TWD", "貨幣", "換算"] | |
| USAGE_TIPS = [ | |
| "提供源貨幣和目標貨幣代碼(如 USD, TWD)", | |
| "支援主要國際貨幣", | |
| "可查詢即時匯率與歷史趨勢" | |
| ] | |
| def get_input_schema(cls) -> Dict[str, Any]: | |
| """獲取輸入參數模式""" | |
| return StandardToolSchemas.create_input_schema({ | |
| "from_currency": { | |
| "type": "string", | |
| "description": "源貨幣代碼 (如 USD, EUR, TWD)", | |
| "default": "USD", | |
| "pattern": "^[A-Z]{3}$" | |
| }, | |
| "to_currency": { | |
| "type": "string", | |
| "description": "目標貨幣代碼 (如 TWD, USD, EUR)", | |
| "default": "TWD", | |
| "pattern": "^[A-Z]{3}$" | |
| }, | |
| "amount": { | |
| "type": "number", | |
| "description": "轉換金額", | |
| "default": 1.0, | |
| "minimum": 0.01 | |
| }, | |
| "conversion": { | |
| "type": "boolean", | |
| "description": "是否進行金額轉換計算", | |
| "default": True | |
| } | |
| }, ["from_currency", "to_currency"]) | |
| def get_output_schema(cls) -> Dict[str, Any]: | |
| """獲取輸出結果模式""" | |
| base_schema = StandardToolSchemas.create_output_schema() | |
| base_schema["properties"].update({ | |
| "rate": {"type": "number"}, | |
| "converted_amount": {"type": ["number", "null"]}, | |
| "raw_data": {"type": "object"} | |
| }) | |
| return base_schema | |
| async def execute(cls, arguments: Dict[str, Any]) -> Dict[str, Any]: | |
| """執行匯率查詢""" | |
| from_currency = arguments.get("from_currency", "USD").upper() | |
| to_currency = arguments.get("to_currency", "TWD").upper() | |
| amount = arguments.get("amount", 1.0) | |
| conversion = arguments.get("conversion", True) | |
| try: | |
| # 獲取匯率數據 | |
| rate_data = await cls._fetch_exchange_rate(from_currency, to_currency) | |
| if rate_data.get("success"): | |
| rate = rate_data.get("rate") | |
| # 生成結構化數據供 AI 格式化 | |
| formatted_text = cls._format_exchange_response( | |
| from_currency, to_currency, rate, amount, conversion, rate_data.get("metadata", {}) | |
| ) | |
| return cls.create_success_response( | |
| content=formatted_text, | |
| data={ | |
| "raw_data": { | |
| "rate": rate, | |
| "from_currency": from_currency, | |
| "to_currency": to_currency, | |
| "amount": amount, | |
| "converted_amount": amount * rate if conversion else None, | |
| "rate_data": rate_data | |
| } | |
| } | |
| ) | |
| else: | |
| return cls.create_error_response( | |
| error=rate_data.get("error", "獲取匯率失敗"), | |
| code="FETCH_ERROR" | |
| ) | |
| except Exception as e: | |
| logger.error(f"匯率查詢錯誤: {e}") | |
| raise ExecutionError(f"匯率查詢時發生錯誤: {str(e)}", e) | |
| async def _fetch_exchange_rate(from_currency: str, to_currency: str) -> Dict[str, Any]: | |
| """獲取匯率數據""" | |
| # 首先嘗試免費 API | |
| try: | |
| result = await ExchangeTool._fetch_from_exchangerate_api(from_currency, to_currency) | |
| if result.get("success"): | |
| return result | |
| except Exception as e: | |
| logger.warning(f"免費匯率 API 失敗: {e}") | |
| # 如果有 Fixer API Key,嘗試備用 API | |
| if FIXER_API_KEY: | |
| try: | |
| result = await ExchangeTool._fetch_from_fixer_api(from_currency, to_currency) | |
| if result.get("success"): | |
| return result | |
| except Exception as e: | |
| logger.warning(f"Fixer API 失敗: {e}") | |
| return { | |
| "success": False, | |
| "error": "所有匯率 API 都無法使用,請稍後再試" | |
| } | |
| async def _fetch_from_exchangerate_api(from_currency: str, to_currency: str) -> Dict[str, Any]: | |
| """從 exchangerate-api.com 獲取匯率""" | |
| url = f"{EXCHANGE_API_BASE}/{from_currency}" | |
| async with aiohttp.ClientSession() as session: | |
| async with session.get(url, timeout=10) as response: | |
| if response.status == 200: | |
| data = await response.json() | |
| rates = data.get("rates", {}) | |
| if to_currency in rates: | |
| rate = rates[to_currency] | |
| return { | |
| "success": True, | |
| "rate": rate, | |
| "metadata": { | |
| "source": "exchangerate-api.com", | |
| "base": data.get("base"), | |
| "date": data.get("date"), | |
| "timestamp": data.get("timestamp") | |
| } | |
| } | |
| else: | |
| return { | |
| "success": False, | |
| "error": f"不支援的貨幣: {to_currency}" | |
| } | |
| else: | |
| return { | |
| "success": False, | |
| "error": f"匯率 API 請求失敗,狀態碼: {response.status}" | |
| } | |
| async def _fetch_from_fixer_api(from_currency: str, to_currency: str) -> Dict[str, Any]: | |
| """從 fixer.io API 獲取匯率""" | |
| url = f"{FIXER_API_BASE}?access_key={FIXER_API_KEY}&base={from_currency}&symbols={to_currency}" | |
| async with aiohttp.ClientSession() as session: | |
| async with session.get(url, timeout=10) as response: | |
| if response.status == 200: | |
| data = await response.json() | |
| if data.get("success"): | |
| rates = data.get("rates", {}) | |
| if to_currency in rates: | |
| rate = rates[to_currency] | |
| return { | |
| "success": True, | |
| "rate": rate, | |
| "metadata": { | |
| "source": "fixer.io", | |
| "base": data.get("base"), | |
| "date": data.get("date"), | |
| "timestamp": data.get("timestamp") | |
| } | |
| } | |
| else: | |
| return { | |
| "success": False, | |
| "error": f"不支援的貨幣: {to_currency}" | |
| } | |
| else: | |
| error_info = data.get("error", {}) | |
| return { | |
| "success": False, | |
| "error": f"Fixer API 錯誤: {error_info.get('info', '未知錯誤')}" | |
| } | |
| else: | |
| return { | |
| "success": False, | |
| "error": f"Fixer API 請求失敗,狀態碼: {response.status}" | |
| } | |
| def _format_exchange_response(from_currency: str, to_currency: str, rate: float, | |
| amount: float, conversion: bool, metadata: Dict) -> str: | |
| """格式化匯率回應""" | |
| # 貨幣符號和名稱映射 | |
| currency_info = ExchangeTool._get_currency_info() | |
| from_symbol = currency_info.get(from_currency, {}).get("symbol", from_currency) | |
| from_name = currency_info.get(from_currency, {}).get("name", from_currency) | |
| to_symbol = currency_info.get(to_currency, {}).get("symbol", to_currency) | |
| to_name = currency_info.get(to_currency, {}).get("name", to_currency) | |
| # 標題 | |
| result = f"💱 匯率資訊\n\n" | |
| # 匯率 | |
| result += f"💰 匯率: 1 {from_currency} = {rate:.4f} {to_currency}\n" | |
| result += f"📈 {from_name} → {to_name}\n\n" | |
| # 如果是轉換計算 | |
| if conversion and amount != 1.0: | |
| converted = amount * rate | |
| result += f"🔄 金額轉換\n" | |
| result += f"📊 {amount:,.2f} {from_currency} = {converted:,.2f} {to_currency}\n" | |
| result += f"💸 {from_symbol}{amount:,.2f} → {to_symbol}{converted:,.2f}\n\n" | |
| # 數據來源和時間 | |
| source = metadata.get("source", "未知") | |
| date = metadata.get("date", "") | |
| if date: | |
| result += f"📅 數據日期: {date}\n" | |
| result += f"🔗 數據來源: {source}\n" | |
| result += f"⏰ 查詢時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" | |
| # 匯率變化提示 | |
| result += f"\n💡 匯率會隨市場變動,僅供參考" | |
| return result | |
| def _get_currency_info() -> Dict[str, Dict[str, str]]: | |
| """獲取貨幣資訊映射""" | |
| return { | |
| "TWD": {"symbol": "NT$", "name": "新台幣"}, | |
| "USD": {"symbol": "$", "name": "美元"}, | |
| "EUR": {"symbol": "€", "name": "歐元"}, | |
| "JPY": {"symbol": "¥", "name": "日圓"}, | |
| "GBP": {"symbol": "£", "name": "英鎊"}, | |
| "AUD": {"symbol": "A$", "name": "澳幣"}, | |
| "CAD": {"symbol": "C$", "name": "加幣"}, | |
| "CHF": {"symbol": "Fr", "name": "瑞士法郎"}, | |
| "CNY": {"symbol": "¥", "name": "人民幣"}, | |
| "HKD": {"symbol": "HK$", "name": "港幣"}, | |
| "NZD": {"symbol": "NZ$", "name": "紐幣"}, | |
| "KRW": {"symbol": "₩", "name": "韓元"}, | |
| "SGD": {"symbol": "S$", "name": "新加坡幣"}, | |
| "THB": {"symbol": "฿", "name": "泰銖"}, | |
| "MYR": {"symbol": "RM", "name": "馬幣"}, | |
| "IDR": {"symbol": "Rp", "name": "印尼盧比"}, | |
| "PHP": {"symbol": "₱", "name": "菲律賓比索"}, | |
| "VND": {"symbol": "₫", "name": "越南盾"}, | |
| "RUB": {"symbol": "₽", "name": "俄羅斯盧布"}, | |
| "BRL": {"symbol": "R$", "name": "巴西雷亞爾"}, | |
| "ZAR": {"symbol": "R", "name": "南非蘭特"}, | |
| "INR": {"symbol": "₹", "name": "印度盧比"} | |
| } |