diff --git a/agent_templates/agents/chain_analysis_agent/chain_analysis_agent.Dockerfile b/agent_templates/agents/chain_analysis_agent/chain_analysis_agent.Dockerfile new file mode 100644 index 0000000..92db719 --- /dev/null +++ b/agent_templates/agents/chain_analysis_agent/chain_analysis_agent.Dockerfile @@ -0,0 +1,39 @@ +# Chain Analysis Agent Dockerfile +# 链上数据分析 Agent - 分析地址活动、交易模式、资金流向 + +FROM python:3.11-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制 common 模块 +COPY common/ ./common/ + +# 复制 Agent 代码 +COPY chain_analysis_agent.py . +COPY requirements.txt . + +# 安装 Python 依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 环境变量 +ENV PYTHONUNBUFFERED=1 +ENV SERVICE_HOST=0.0.0.0 +ENV SERVICE_PORT=8000 +ENV POD_NAME=chain-analysis-agent +ENV LLM_BASE_URL=https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1 +ENV LLM_MODEL=taiji/gpt-4o-mini + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# 暴露端口 +EXPOSE 8000 + +# 运行 +CMD ["python", "chain_analysis_agent.py"] diff --git a/agent_templates/agents/chain_analysis_agent/chain_analysis_agent.py b/agent_templates/agents/chain_analysis_agent/chain_analysis_agent.py new file mode 100644 index 0000000..d92b126 --- /dev/null +++ b/agent_templates/agents/chain_analysis_agent/chain_analysis_agent.py @@ -0,0 +1,874 @@ +""" +Chain Analysis Agent - 链上数据分析 Agent +分析区块链地址活动、交易模式、资金流向、合约交互等 +支持 Ethereum, BSC, Polygon 等 EVM 兼容链 +""" +import os +import sys +import logging +import aiohttp +from typing import Optional, List, Dict, Any +from datetime import datetime, timedelta +from collections import defaultdict + +from fastapi import FastAPI, HTTPException, Query, Header, Request +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +import uvicorn + +# 添加 common 模块路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# 导入回调工具 +try: + from common.agent_callback_utils import AgentCallbackHandler, CallbackContextManager + CALLBACK_ENABLED = True +except ImportError: + CALLBACK_ENABLED = False + AgentCallbackHandler = None + CallbackContextManager = None + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# 环境变量 +SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") +SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) +POD_NAME = os.getenv("POD_NAME", "chain-analysis-agent") +USER_ID = os.getenv("USER_ID", "") + +# LLM 配置 +LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1") +LLM_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini") + +# 支持的区块链网络配置 (Etherscan V2 API) +CHAIN_CONFIGS = { + "ethereum": { + "name": "Ethereum", + "symbol": "ETH", + "decimals": 18, + "chainid": 1, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://etherscan.io" + }, + "bsc": { + "name": "BNB Smart Chain", + "symbol": "BNB", + "decimals": 18, + "chainid": 56, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://bscscan.com" + }, + "polygon": { + "name": "Polygon", + "symbol": "POL", + "decimals": 18, + "chainid": 137, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://polygonscan.com" + }, + "arbitrum": { + "name": "Arbitrum", + "symbol": "ETH", + "decimals": 18, + "chainid": 42161, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://arbiscan.io" + }, + "optimism": { + "name": "Optimism", + "symbol": "ETH", + "decimals": 18, + "chainid": 10, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://optimistic.etherscan.io" + }, + "base": { + "name": "Base", + "symbol": "ETH", + "decimals": 18, + "chainid": 8453, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://basescan.org" + } +} + +# FastAPI 应用 +app = FastAPI( + title="Chain Analysis Agent", + description="链上数据分析 - 分析地址活动、交易模式、资金流向", + version="1.0.0" +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 回调处理器 +callback_handler: Optional[AgentCallbackHandler] = None + + +# ==================== 请求/响应模型 ==================== + +class AddressAnalysisRequest(BaseModel): + """地址分析请求""" + address: str = Field(..., description="钱包地址") + chain: str = Field("ethereum", description="区块链网络") + days: int = Field(30, ge=1, le=365, description="分析天数") + user_id: Optional[str] = Field(None, description="用户ID") + + +class TransactionPatternRequest(BaseModel): + """交易模式分析请求""" + address: str = Field(..., description="钱包地址") + chain: str = Field("ethereum", description="区块链网络") + user_id: Optional[str] = Field(None, description="用户ID") + + +class FundFlowRequest(BaseModel): + """资金流向分析请求""" + address: str = Field(..., description="钱包地址") + chain: str = Field("ethereum", description="区块链网络") + limit: int = Field(100, ge=10, le=500, description="交易数量") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ContractInteractionRequest(BaseModel): + """合约交互分析请求""" + address: str = Field(..., description="钱包地址") + chain: str = Field("ethereum", description="区块链网络") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ChatRequest(BaseModel): + """Chat 请求""" + message: str = Field(..., description="用户消息") + chain: str = Field("ethereum", description="默认区块链网络") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ChatResponse(BaseModel): + """Chat 响应""" + response: str + analysis: Optional[Dict[str, Any]] = None + timestamp: str + + +class HealthResponse(BaseModel): + """健康检查响应""" + status: str + pod_name: str + supported_chains: List[str] + callback_enabled: bool + timestamp: str + + +# ==================== 生命周期 ==================== + +@app.on_event("startup") +async def startup_event(): + """应用启动时初始化回调处理器""" + global callback_handler + + if CALLBACK_ENABLED and AgentCallbackHandler: + try: + callback_handler = AgentCallbackHandler( + agent_name=POD_NAME, + user_id=USER_ID + ) + logger.info(f"回调处理器已初始化: agent={POD_NAME}, user={USER_ID}") + except Exception as e: + logger.warning(f"回调处理器初始化失败: {e}") + + logger.info(f"Chain Analysis Agent 启动完成 - {POD_NAME}") + logger.info(f"支持的区块链: {list(CHAIN_CONFIGS.keys())}") + + +# ==================== 核心分析功能 ==================== + +async def fetch_all_transactions(address: str, chain: str, api_key: str, limit: int = 200) -> List[Dict]: + """获取所有交易用于分析""" + if chain not in CHAIN_CONFIGS: + return [] + + config = CHAIN_CONFIGS[chain] + url = config["api_url"] + + params = { + "chainid": config["chainid"], + "module": "account", + "action": "txlist", + "address": address, + "startblock": 0, + "endblock": 99999999, + "page": 1, + "offset": limit, + "sort": "desc", + "apikey": api_key + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=20)) as response: + if response.status == 200: + data = await response.json() + if data.get("status") == "1": + return data.get("result", []) + except Exception as e: + logger.error(f"获取交易失败: {e}") + return [] + + +async def fetch_internal_transactions(address: str, chain: str, api_key: str) -> List[Dict]: + """获取内部交易""" + if chain not in CHAIN_CONFIGS: + return [] + + config = CHAIN_CONFIGS[chain] + url = config["api_url"] + + params = { + "chainid": config["chainid"], + "module": "account", + "action": "txlistinternal", + "address": address, + "startblock": 0, + "endblock": 99999999, + "page": 1, + "offset": 100, + "sort": "desc", + "apikey": api_key + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as response: + if response.status == 200: + data = await response.json() + if data.get("status") == "1": + return data.get("result", []) + except Exception as e: + logger.error(f"获取内部交易失败: {e}") + return [] + + +async def fetch_balance(address: str, chain: str, api_key: str) -> float: + """获取余额""" + if chain not in CHAIN_CONFIGS: + return 0.0 + + config = CHAIN_CONFIGS[chain] + url = config["api_url"] + + params = { + "chainid": config["chainid"], + "module": "account", + "action": "balance", + "address": address, + "tag": "latest", + "apikey": api_key + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response: + if response.status == 200: + data = await response.json() + if data.get("status") == "1": + balance_wei = int(data.get("result", 0)) + return balance_wei / (10 ** config["decimals"]) + except Exception as e: + logger.error(f"获取余额失败: {e}") + return 0.0 + + +def analyze_address_activity(transactions: List[Dict], address: str, chain: str, days: int = 30) -> Dict[str, Any]: + """分析地址活动""" + config = CHAIN_CONFIGS.get(chain, CHAIN_CONFIGS["ethereum"]) + address_lower = address.lower() + + now = datetime.utcnow() + cutoff = now - timedelta(days=days) + + # 统计数据 + total_sent = 0.0 + total_received = 0.0 + tx_count_in = 0 + tx_count_out = 0 + unique_addresses = set() + failed_tx = 0 + daily_activity = defaultdict(lambda: {"in": 0, "out": 0, "count": 0}) + + for tx in transactions: + try: + timestamp = datetime.fromtimestamp(int(tx.get("timeStamp", 0))) + if timestamp < cutoff: + continue + + value_wei = int(tx.get("value", 0)) + value = value_wei / (10 ** config["decimals"]) + + day_key = timestamp.strftime("%Y-%m-%d") + daily_activity[day_key]["count"] += 1 + + if tx.get("isError") == "1": + failed_tx += 1 + continue + + from_addr = tx.get("from", "").lower() + to_addr = tx.get("to", "").lower() + + if from_addr == address_lower: + # 发出 + total_sent += value + tx_count_out += 1 + daily_activity[day_key]["out"] += value + if to_addr: + unique_addresses.add(to_addr) + elif to_addr == address_lower: + # 收到 + total_received += value + tx_count_in += 1 + daily_activity[day_key]["in"] += value + unique_addresses.add(from_addr) + except Exception as e: + logger.error(f"解析交易失败: {e}") + + # 计算活跃天数 + active_days = len(daily_activity) + + return { + "address": address, + "chain": chain, + "period_days": days, + "summary": { + "total_sent": round(total_sent, 6), + "total_received": round(total_received, 6), + "net_flow": round(total_received - total_sent, 6), + "tx_count_in": tx_count_in, + "tx_count_out": tx_count_out, + "total_tx": tx_count_in + tx_count_out, + "failed_tx": failed_tx, + "unique_addresses": len(unique_addresses), + "active_days": active_days + }, + "symbol": config["symbol"], + "daily_activity": dict(sorted(daily_activity.items(), reverse=True)[:7]) # 最近7天 + } + + +def analyze_transaction_patterns(transactions: List[Dict], address: str, chain: str) -> Dict[str, Any]: + """分析交易模式""" + config = CHAIN_CONFIGS.get(chain, CHAIN_CONFIGS["ethereum"]) + address_lower = address.lower() + + # 时间分布 + hourly_distribution = defaultdict(int) + daily_distribution = defaultdict(int) + + # 金额分布 + value_ranges = { + "micro": 0, # < 0.01 + "small": 0, # 0.01 - 0.1 + "medium": 0, # 0.1 - 1 + "large": 0, # 1 - 10 + "whale": 0 # > 10 + } + + # 交互地址频率 + address_frequency = defaultdict(int) + + # 交易间隔 + timestamps = [] + + for tx in transactions: + try: + timestamp = datetime.fromtimestamp(int(tx.get("timeStamp", 0))) + timestamps.append(timestamp) + + hourly_distribution[timestamp.hour] += 1 + daily_distribution[timestamp.strftime("%A")] += 1 + + value_wei = int(tx.get("value", 0)) + value = value_wei / (10 ** config["decimals"]) + + if value < 0.01: + value_ranges["micro"] += 1 + elif value < 0.1: + value_ranges["small"] += 1 + elif value < 1: + value_ranges["medium"] += 1 + elif value < 10: + value_ranges["large"] += 1 + else: + value_ranges["whale"] += 1 + + from_addr = tx.get("from", "").lower() + to_addr = tx.get("to", "").lower() + + counterparty = to_addr if from_addr == address_lower else from_addr + if counterparty: + address_frequency[counterparty] += 1 + except Exception as e: + logger.error(f"解析交易失败: {e}") + + # 计算交易间隔 + avg_interval = None + if len(timestamps) > 1: + timestamps.sort(reverse=True) + intervals = [] + for i in range(len(timestamps) - 1): + interval = (timestamps[i] - timestamps[i+1]).total_seconds() / 3600 # 小时 + intervals.append(interval) + avg_interval = round(sum(intervals) / len(intervals), 2) + + # 前5个交互地址 + top_addresses = sorted(address_frequency.items(), key=lambda x: x[1], reverse=True)[:5] + + return { + "address": address, + "chain": chain, + "patterns": { + "hourly_distribution": dict(hourly_distribution), + "daily_distribution": dict(daily_distribution), + "value_distribution": value_ranges, + "avg_interval_hours": avg_interval, + "top_counterparties": [{"address": addr, "tx_count": count} for addr, count in top_addresses] + }, + "behavior_summary": generate_behavior_summary(hourly_distribution, value_ranges, avg_interval) + } + + +def generate_behavior_summary(hourly: Dict, values: Dict, interval: Optional[float]) -> str: + """生成行为摘要""" + summary_parts = [] + + # 活跃时段 + if hourly: + peak_hour = max(hourly, key=hourly.get) + summary_parts.append(f"活跃高峰时段: {peak_hour}:00 UTC") + + # 交易规模 + total_tx = sum(values.values()) + if total_tx > 0: + whale_ratio = values["whale"] / total_tx * 100 + if whale_ratio > 20: + summary_parts.append("大额交易频繁(可能是机构或巨鲸)") + elif values["micro"] / total_tx > 0.5: + summary_parts.append("以小额交易为主(可能是频繁交易者或机器人)") + + # 交易频率 + if interval: + if interval < 1: + summary_parts.append("高频交易(可能是自动化程序)") + elif interval > 168: # 一周 + summary_parts.append("低频交易(普通持有者)") + + return "; ".join(summary_parts) if summary_parts else "交易模式正常" + + +def analyze_fund_flow(transactions: List[Dict], address: str, chain: str) -> Dict[str, Any]: + """分析资金流向""" + config = CHAIN_CONFIGS.get(chain, CHAIN_CONFIGS["ethereum"]) + address_lower = address.lower() + + inflow = defaultdict(float) # 资金来源 + outflow = defaultdict(float) # 资金去向 + + for tx in transactions: + try: + if tx.get("isError") == "1": + continue + + value_wei = int(tx.get("value", 0)) + value = value_wei / (10 ** config["decimals"]) + + if value == 0: + continue + + from_addr = tx.get("from", "").lower() + to_addr = tx.get("to", "").lower() + + if from_addr == address_lower and to_addr: + outflow[to_addr] += value + elif to_addr == address_lower: + inflow[from_addr] += value + except Exception as e: + logger.error(f"解析交易失败: {e}") + + # 排序获取 Top 10 + top_inflow = sorted(inflow.items(), key=lambda x: x[1], reverse=True)[:10] + top_outflow = sorted(outflow.items(), key=lambda x: x[1], reverse=True)[:10] + + total_in = sum(inflow.values()) + total_out = sum(outflow.values()) + + return { + "address": address, + "chain": chain, + "fund_flow": { + "total_inflow": round(total_in, 6), + "total_outflow": round(total_out, 6), + "net_flow": round(total_in - total_out, 6), + "inflow_sources": len(inflow), + "outflow_destinations": len(outflow), + "top_inflow": [ + {"address": addr, "amount": round(amt, 6), "symbol": config["symbol"]} + for addr, amt in top_inflow + ], + "top_outflow": [ + {"address": addr, "amount": round(amt, 6), "symbol": config["symbol"]} + for addr, amt in top_outflow + ] + }, + "symbol": config["symbol"] + } + + +def analyze_contract_interactions(transactions: List[Dict], address: str, chain: str) -> Dict[str, Any]: + """分析合约交互""" + config = CHAIN_CONFIGS.get(chain, CHAIN_CONFIGS["ethereum"]) + address_lower = address.lower() + + contract_interactions = defaultdict(lambda: {"count": 0, "methods": set(), "value": 0.0}) + + for tx in transactions: + try: + from_addr = tx.get("from", "").lower() + to_addr = tx.get("to", "").lower() + + # 只分析发出的交易且有 input data 的(合约调用) + if from_addr != address_lower: + continue + + input_data = tx.get("input", "") + if input_data and input_data != "0x" and len(input_data) >= 10: + method_id = input_data[:10] + value_wei = int(tx.get("value", 0)) + value = value_wei / (10 ** config["decimals"]) + + contract_interactions[to_addr]["count"] += 1 + contract_interactions[to_addr]["methods"].add(method_id) + contract_interactions[to_addr]["value"] += value + except Exception as e: + logger.error(f"解析交易失败: {e}") + + # 排序 + sorted_contracts = sorted( + contract_interactions.items(), + key=lambda x: x[1]["count"], + reverse=True + )[:10] + + return { + "address": address, + "chain": chain, + "contract_interactions": { + "total_contracts": len(contract_interactions), + "top_contracts": [ + { + "contract": addr, + "interaction_count": data["count"], + "unique_methods": len(data["methods"]), + "total_value": round(data["value"], 6), + "symbol": config["symbol"], + "explorer_url": f"{config['explorer_url']}/address/{addr}" + } + for addr, data in sorted_contracts + ] + } + } + + +async def chat_with_llm(message: str, context: str, api_key: str) -> str: + """调用 LLM 生成分析报告""" + try: + async with aiohttp.ClientSession() as session: + payload = { + "model": LLM_MODEL, + "messages": [ + { + "role": "system", + "content": """你是一个资深的区块链数据分析师,擅长: +1. 分析钱包地址的链上行为模式 +2. 识别交易特征(高频交易、巨鲸、机器人等) +3. 追踪资金流向和来源 +4. 分析合约交互行为 +5. 提供风险评估和投资建议 + +请根据链上数据提供专业、深入的分析报告,用简洁易懂的语言表达。""" + }, + { + "role": "user", + "content": f"链上分析数据:\n{context}\n\n分析请求: {message}" + } + ], + "max_tokens": 800, + "temperature": 0.7 + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + async with session.post( + f"{LLM_BASE_URL}/chat/completions", + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30) + ) as response: + if response.status == 200: + data = await response.json() + return data.get("choices", [{}])[0].get("message", {}).get("content", "抱歉,无法生成分析报告") + else: + error = await response.text() + logger.error(f"LLM 请求失败: {response.status} - {error}") + return f"LLM 服务错误: {response.status}" + except Exception as e: + logger.error(f"LLM 调用失败: {e}") + return f"分析失败: {str(e)}" + + +def extract_address_from_message(message: str) -> Optional[str]: + """从消息中提取以太坊地址""" + import re + pattern = r'0x[a-fA-F0-9]{40}' + match = re.search(pattern, message) + return match.group(0) if match else None + + +# ==================== API 端点 ==================== + +@app.get("/", response_model=dict) +async def root(): + """服务状态""" + return { + "service": "Chain Analysis Agent", + "description": "链上数据分析 - 分析地址活动、交易模式、资金流向", + "status": "running", + "supported_chains": list(CHAIN_CONFIGS.keys()), + "tools": ["address_analysis", "transaction_patterns", "fund_flow", "contract_interactions", "chat"] + } + + +@app.get("/health", response_model=HealthResponse) +async def health_check(): + """健康检查""" + return HealthResponse( + status="healthy", + pod_name=POD_NAME, + supported_chains=list(CHAIN_CONFIGS.keys()), + callback_enabled=CALLBACK_ENABLED, + timestamp=datetime.utcnow().isoformat() + ) + + +@app.post("/address-analysis") +async def address_analysis( + request: AddressAnalysisRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key") +): + """地址活动分析""" + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + transactions = await fetch_all_transactions(request.address, request.chain, scan_key) + + if not transactions: + raise HTTPException(status_code=404, detail="未找到交易记录") + + result = analyze_address_activity(transactions, request.address, request.chain, request.days) + balance = await fetch_balance(request.address, request.chain, scan_key) + result["current_balance"] = round(balance, 8) + + return { + **result, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.post("/transaction-patterns") +async def transaction_patterns( + request: TransactionPatternRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key") +): + """交易模式分析""" + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + transactions = await fetch_all_transactions(request.address, request.chain, scan_key) + + if not transactions: + raise HTTPException(status_code=404, detail="未找到交易记录") + + result = analyze_transaction_patterns(transactions, request.address, request.chain) + + return { + **result, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.post("/fund-flow") +async def fund_flow( + request: FundFlowRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key") +): + """资金流向分析""" + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + transactions = await fetch_all_transactions(request.address, request.chain, scan_key, request.limit) + + if not transactions: + raise HTTPException(status_code=404, detail="未找到交易记录") + + result = analyze_fund_flow(transactions, request.address, request.chain) + + return { + **result, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.post("/contract-interactions") +async def contract_interactions( + request: ContractInteractionRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key") +): + """合约交互分析""" + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + transactions = await fetch_all_transactions(request.address, request.chain, scan_key) + + if not transactions: + raise HTTPException(status_code=404, detail="未找到交易记录") + + result = analyze_contract_interactions(transactions, request.address, request.chain) + + return { + **result, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.post("/chat", response_model=ChatResponse) +async def chat( + request: ChatRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key"), + llm_key: Optional[str] = Header(None, alias="llm-key"), + authorization: Optional[str] = Header(None) +): + """智能对话 - 支持自然语言分析链上数据 + + api_key 通过请求头传递: + - api-key 或 etherscan-key: 区块链浏览器 API Key + - llm-key 或 Authorization: LLM API Key + """ + # 获取区块链 API Key + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + # 获取 LLM API Key + llm_api_key = llm_key + if not llm_api_key and authorization: + if authorization.startswith("Bearer "): + llm_api_key = authorization[7:] + else: + llm_api_key = authorization + + if not llm_api_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 llm-key 或 Authorization") + + # 从消息中提取地址 + address = extract_address_from_message(request.message) + + analysis_data = {} + if address: + transactions = await fetch_all_transactions(address, request.chain, scan_key) + + if transactions: + # 执行全面分析 + analysis_data["activity"] = analyze_address_activity(transactions, address, request.chain) + analysis_data["patterns"] = analyze_transaction_patterns(transactions, address, request.chain) + analysis_data["fund_flow"] = analyze_fund_flow(transactions, address, request.chain) + analysis_data["contracts"] = analyze_contract_interactions(transactions, address, request.chain) + analysis_data["balance"] = await fetch_balance(address, request.chain, scan_key) + + # 构建上下文 + if analysis_data: + context_parts = [] + if "activity" in analysis_data: + s = analysis_data["activity"]["summary"] + context_parts.append(f"地址: {address}") + context_parts.append(f"当前余额: {analysis_data['balance']:.6f} ETH") + context_parts.append(f"30天活动: 收入 {s['total_received']:.4f} ETH, 支出 {s['total_sent']:.4f} ETH") + context_parts.append(f"交易统计: 入账 {s['tx_count_in']} 笔, 出账 {s['tx_count_out']} 笔") + if "patterns" in analysis_data: + p = analysis_data["patterns"] + context_parts.append(f"行为特征: {p['behavior_summary']}") + if "fund_flow" in analysis_data: + f = analysis_data["fund_flow"]["fund_flow"] + context_parts.append(f"资金来源数: {f['inflow_sources']}, 去向数: {f['outflow_destinations']}") + if "contracts" in analysis_data: + c = analysis_data["contracts"]["contract_interactions"] + context_parts.append(f"交互合约数: {c['total_contracts']}") + context = "\n".join(context_parts) + else: + context = "未检测到有效的钱包地址,请提供 0x 开头的以太坊地址" + + # 调用 LLM 生成分析报告 + llm_response = await chat_with_llm(request.message, context, llm_api_key) + + return ChatResponse( + response=llm_response, + analysis=analysis_data if analysis_data else {"detected_address": address}, + timestamp=datetime.utcnow().isoformat() + ) + + +@app.get("/chains") +async def list_chains(): + """列出支持的区块链""" + return { + "chains": [ + { + "id": chain_id, + "name": config["name"], + "symbol": config["symbol"], + "explorer": config["explorer_url"] + } + for chain_id, config in CHAIN_CONFIGS.items() + ] + } + + +# ==================== 主入口 ==================== + +def main(): + """主函数""" + logger.info(f"启动 Chain Analysis Agent - {POD_NAME}") + logger.info(f"支持的区块链: {list(CHAIN_CONFIGS.keys())}") + logger.info(f"回调功能: {'已启用' if CALLBACK_ENABLED else '未启用'}") + + uvicorn.run(app, host=SERVICE_HOST, port=SERVICE_PORT, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/agent_templates/agents/chain_analysis_agent/requirements.txt b/agent_templates/agents/chain_analysis_agent/requirements.txt new file mode 100644 index 0000000..d508938 --- /dev/null +++ b/agent_templates/agents/chain_analysis_agent/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.104.0 +uvicorn>=0.24.0 +aiohttp>=3.9.0 +pydantic>=2.0.0 +python-multipart>=0.0.6 +httpx>=0.25.0 diff --git a/agent_templates/agents/chain_explorer_agent/chain_explorer_agent.Dockerfile b/agent_templates/agents/chain_explorer_agent/chain_explorer_agent.Dockerfile new file mode 100644 index 0000000..27a1b82 --- /dev/null +++ b/agent_templates/agents/chain_explorer_agent/chain_explorer_agent.Dockerfile @@ -0,0 +1,39 @@ +# Chain Explorer Agent Dockerfile +# 链上数据查询 Agent - 查询地址余额、交易记录、代币信息 + +FROM python:3.11-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制 common 模块 +COPY common/ ./common/ + +# 复制 Agent 代码 +COPY chain_explorer_agent.py . +COPY requirements.txt . + +# 安装 Python 依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 环境变量 +ENV PYTHONUNBUFFERED=1 +ENV SERVICE_HOST=0.0.0.0 +ENV SERVICE_PORT=8000 +ENV POD_NAME=chain-explorer-agent +ENV LLM_BASE_URL=https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1 +ENV LLM_MODEL=taiji/gpt-4o-mini + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# 暴露端口 +EXPOSE 8000 + +# 运行 +CMD ["python", "chain_explorer_agent.py"] diff --git a/agent_templates/agents/chain_explorer_agent/chain_explorer_agent.py b/agent_templates/agents/chain_explorer_agent/chain_explorer_agent.py new file mode 100644 index 0000000..c25742d --- /dev/null +++ b/agent_templates/agents/chain_explorer_agent/chain_explorer_agent.py @@ -0,0 +1,613 @@ +""" +Chain Explorer Agent - 链上数据查询 Agent +查询区块链地址余额、交易记录、代币信息等 +支持 Ethereum, BSC, Polygon 等 EVM 兼容链 +""" +import os +import sys +import logging +import aiohttp +from typing import Optional, List, Dict, Any +from datetime import datetime +from decimal import Decimal + +from fastapi import FastAPI, HTTPException, Query, Header, Request +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +import uvicorn + +# 添加 common 模块路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# 导入回调工具 +try: + from common.agent_callback_utils import AgentCallbackHandler, CallbackContextManager + CALLBACK_ENABLED = True +except ImportError: + CALLBACK_ENABLED = False + AgentCallbackHandler = None + CallbackContextManager = None + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# 环境变量 +SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") +SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) +POD_NAME = os.getenv("POD_NAME", "chain-explorer-agent") +USER_ID = os.getenv("USER_ID", "") + +# LLM 配置 +LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1") +LLM_MODEL = os.getenv("LLM_MODEL", "taiji/gpt-4o-mini") + +# 支持的区块链网络配置 (Etherscan V2 API) +CHAIN_CONFIGS = { + "ethereum": { + "name": "Ethereum", + "symbol": "ETH", + "decimals": 18, + "chainid": 1, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://etherscan.io" + }, + "bsc": { + "name": "BNB Smart Chain", + "symbol": "BNB", + "decimals": 18, + "chainid": 56, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://bscscan.com" + }, + "polygon": { + "name": "Polygon", + "symbol": "POL", + "decimals": 18, + "chainid": 137, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://polygonscan.com" + }, + "arbitrum": { + "name": "Arbitrum", + "symbol": "ETH", + "decimals": 18, + "chainid": 42161, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://arbiscan.io" + }, + "optimism": { + "name": "Optimism", + "symbol": "ETH", + "decimals": 18, + "chainid": 10, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://optimistic.etherscan.io" + }, + "base": { + "name": "Base", + "symbol": "ETH", + "decimals": 18, + "chainid": 8453, + "api_url": "https://api.etherscan.io/v2/api", + "explorer_url": "https://basescan.org" + } +} + +# FastAPI 应用 +app = FastAPI( + title="Chain Explorer Agent", + description="链上数据查询 - 查询地址余额、交易记录、代币信息", + version="1.0.0" +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 回调处理器 +callback_handler: Optional[AgentCallbackHandler] = None + + +# ==================== 请求/响应模型 ==================== + +class BalanceRequest(BaseModel): + """余额查询请求""" + address: str = Field(..., description="钱包地址") + chain: str = Field("ethereum", description="区块链网络: ethereum, bsc, polygon, arbitrum, optimism") + user_id: Optional[str] = Field(None, description="用户ID") + + +class BalanceResponse(BaseModel): + """余额响应""" + address: str + chain: str + balance: str + balance_formatted: str + symbol: str + usd_value: Optional[float] = None + timestamp: str + + +class TransactionRequest(BaseModel): + """交易查询请求""" + address: str = Field(..., description="钱包地址") + chain: str = Field("ethereum", description="区块链网络") + page: int = Field(1, ge=1, description="页码") + limit: int = Field(10, ge=1, le=100, description="每页数量") + user_id: Optional[str] = Field(None, description="用户ID") + + +class TokenBalanceRequest(BaseModel): + """代币余额查询请求""" + address: str = Field(..., description="钱包地址") + chain: str = Field("ethereum", description="区块链网络") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ChatRequest(BaseModel): + """Chat 请求""" + message: str = Field(..., description="用户消息") + chain: str = Field("ethereum", description="默认区块链网络") + user_id: Optional[str] = Field(None, description="用户ID") + + +class ChatResponse(BaseModel): + """Chat 响应""" + response: str + data: Optional[Dict[str, Any]] = None + timestamp: str + + +class HealthResponse(BaseModel): + """健康检查响应""" + status: str + pod_name: str + supported_chains: List[str] + callback_enabled: bool + timestamp: str + + +# ==================== 生命周期 ==================== + +@app.on_event("startup") +async def startup_event(): + """应用启动时初始化回调处理器""" + global callback_handler + + if CALLBACK_ENABLED and AgentCallbackHandler: + try: + callback_handler = AgentCallbackHandler( + agent_name=POD_NAME, + user_id=USER_ID + ) + logger.info(f"回调处理器已初始化: agent={POD_NAME}, user={USER_ID}") + except Exception as e: + logger.warning(f"回调处理器初始化失败: {e}") + + logger.info(f"Chain Explorer Agent 启动完成 - {POD_NAME}") + logger.info(f"支持的区块链: {list(CHAIN_CONFIGS.keys())}") + + +# ==================== 核心功能 ==================== + +async def fetch_balance(address: str, chain: str, api_key: str) -> Dict[str, Any]: + """获取地址余额""" + if chain not in CHAIN_CONFIGS: + return {"success": False, "error": f"不支持的区块链: {chain}"} + + config = CHAIN_CONFIGS[chain] + url = config["api_url"] + + params = { + "chainid": config["chainid"], + "module": "account", + "action": "balance", + "address": address, + "tag": "latest", + "apikey": api_key + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as response: + if response.status == 200: + data = await response.json() + if data.get("status") == "1": + balance_wei = int(data.get("result", 0)) + balance_eth = balance_wei / (10 ** config["decimals"]) + return { + "success": True, + "address": address, + "chain": chain, + "chain_name": config["name"], + "balance_wei": str(balance_wei), + "balance": round(balance_eth, 8), + "symbol": config["symbol"], + "explorer_url": f"{config['explorer_url']}/address/{address}" + } + else: + return {"success": False, "error": data.get("message", "API 错误")} + else: + return {"success": False, "error": f"HTTP {response.status}"} + except Exception as e: + logger.error(f"获取余额失败: {e}") + return {"success": False, "error": str(e)} + + +async def fetch_transactions(address: str, chain: str, api_key: str, page: int = 1, limit: int = 10) -> Dict[str, Any]: + """获取交易记录""" + if chain not in CHAIN_CONFIGS: + return {"success": False, "error": f"不支持的区块链: {chain}"} + + config = CHAIN_CONFIGS[chain] + url = config["api_url"] + + params = { + "chainid": config["chainid"], + "module": "account", + "action": "txlist", + "address": address, + "startblock": 0, + "endblock": 99999999, + "page": page, + "offset": limit, + "sort": "desc", + "apikey": api_key + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as response: + if response.status == 200: + data = await response.json() + if data.get("status") == "1": + transactions = [] + for tx in data.get("result", []): + value_wei = int(tx.get("value", 0)) + value_eth = value_wei / (10 ** config["decimals"]) + transactions.append({ + "hash": tx.get("hash"), + "block": tx.get("blockNumber"), + "timestamp": datetime.fromtimestamp(int(tx.get("timeStamp", 0))).isoformat(), + "from": tx.get("from"), + "to": tx.get("to"), + "value": round(value_eth, 8), + "symbol": config["symbol"], + "gas_used": tx.get("gasUsed"), + "gas_price": tx.get("gasPrice"), + "is_error": tx.get("isError") == "1", + "tx_url": f"{config['explorer_url']}/tx/{tx.get('hash')}" + }) + return { + "success": True, + "address": address, + "chain": chain, + "transactions": transactions, + "count": len(transactions), + "page": page + } + else: + return {"success": False, "error": data.get("message", "API 错误")} + else: + return {"success": False, "error": f"HTTP {response.status}"} + except Exception as e: + logger.error(f"获取交易失败: {e}") + return {"success": False, "error": str(e)} + + +async def fetch_token_balances(address: str, chain: str, api_key: str) -> Dict[str, Any]: + """获取 ERC20 代币余额""" + if chain not in CHAIN_CONFIGS: + return {"success": False, "error": f"不支持的区块链: {chain}"} + + config = CHAIN_CONFIGS[chain] + url = config["api_url"] + + params = { + "chainid": config["chainid"], + "module": "account", + "action": "tokentx", + "address": address, + "page": 1, + "offset": 100, + "sort": "desc", + "apikey": api_key + } + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=15)) as response: + if response.status == 200: + data = await response.json() + if data.get("status") == "1": + # 统计代币 + token_map = {} + for tx in data.get("result", []): + contract = tx.get("contractAddress") + if contract not in token_map: + token_map[contract] = { + "contract": contract, + "name": tx.get("tokenName"), + "symbol": tx.get("tokenSymbol"), + "decimals": int(tx.get("tokenDecimal", 18)), + "tx_count": 0 + } + token_map[contract]["tx_count"] += 1 + + tokens = list(token_map.values()) + return { + "success": True, + "address": address, + "chain": chain, + "tokens": tokens, + "token_count": len(tokens) + } + else: + return {"success": True, "address": address, "chain": chain, "tokens": [], "token_count": 0} + else: + return {"success": False, "error": f"HTTP {response.status}"} + except Exception as e: + logger.error(f"获取代币失败: {e}") + return {"success": False, "error": str(e)} + + +async def chat_with_llm(message: str, context: str, api_key: str) -> str: + """调用 LLM 生成响应""" + try: + async with aiohttp.ClientSession() as session: + payload = { + "model": LLM_MODEL, + "messages": [ + { + "role": "system", + "content": """你是一个专业的区块链数据分析师。你可以: +1. 查询钱包地址的余额和交易记录 +2. 分析地址的链上活动 +3. 解答关于以太坊、BSC、Polygon等EVM链的问题 + +请根据提供的链上数据,用简洁专业的语言回答用户问题。""" + }, + { + "role": "user", + "content": f"链上数据:\n{context}\n\n用户问题: {message}" + } + ], + "max_tokens": 500, + "temperature": 0.7 + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + async with session.post( + f"{LLM_BASE_URL}/chat/completions", + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30) + ) as response: + if response.status == 200: + data = await response.json() + return data.get("choices", [{}])[0].get("message", {}).get("content", "抱歉,无法生成回复") + else: + error = await response.text() + logger.error(f"LLM 请求失败: {response.status} - {error}") + return f"LLM 服务错误: {response.status}" + except Exception as e: + logger.error(f"LLM 调用失败: {e}") + return f"调用失败: {str(e)}" + + +def extract_address_from_message(message: str) -> Optional[str]: + """从消息中提取以太坊地址""" + import re + # 匹配以太坊地址格式 (0x开头,40个十六进制字符) + pattern = r'0x[a-fA-F0-9]{40}' + match = re.search(pattern, message) + return match.group(0) if match else None + + +# ==================== API 端点 ==================== + +@app.get("/", response_model=dict) +async def root(): + """服务状态""" + return { + "service": "Chain Explorer Agent", + "description": "链上数据查询 - 查询地址余额、交易记录、代币信息", + "status": "running", + "supported_chains": list(CHAIN_CONFIGS.keys()), + "tools": ["balance", "transactions", "tokens", "chat"] + } + + +@app.get("/health", response_model=HealthResponse) +async def health_check(): + """健康检查""" + return HealthResponse( + status="healthy", + pod_name=POD_NAME, + supported_chains=list(CHAIN_CONFIGS.keys()), + callback_enabled=CALLBACK_ENABLED, + timestamp=datetime.utcnow().isoformat() + ) + + +@app.post("/balance") +async def get_balance( + request: BalanceRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key") +): + """查询地址余额""" + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + result = await fetch_balance(request.address, request.chain, scan_key) + + if not result["success"]: + raise HTTPException(status_code=400, detail=result["error"]) + + return { + **result, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.post("/transactions") +async def get_transactions( + request: TransactionRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key") +): + """查询交易记录""" + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + result = await fetch_transactions(request.address, request.chain, scan_key, request.page, request.limit) + + if not result["success"]: + raise HTTPException(status_code=400, detail=result["error"]) + + return { + **result, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.post("/tokens") +async def get_token_balances( + request: TokenBalanceRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key") +): + """查询代币余额""" + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + result = await fetch_token_balances(request.address, request.chain, scan_key) + + if not result["success"]: + raise HTTPException(status_code=400, detail=result["error"]) + + return { + **result, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.post("/chat", response_model=ChatResponse) +async def chat( + request: ChatRequest, + api_key: Optional[str] = Header(None, alias="api-key"), + etherscan_key: Optional[str] = Header(None, alias="etherscan-key"), + llm_key: Optional[str] = Header(None, alias="llm-key"), + authorization: Optional[str] = Header(None) +): + """智能对话 - 支持自然语言查询链上数据 + + api_key 通过请求头传递: + - api-key: 区块链浏览器 API Key (Etherscan 等) + - etherscan-key: Etherscan API Key (优先) + - llm-key: LLM API Key (用于 AI 分析) + - Authorization: Bearer LLM-API-Key + """ + # 获取区块链 API Key + scan_key = etherscan_key or api_key + if not scan_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 etherscan-key 或 api-key") + + # 获取 LLM API Key + llm_api_key = llm_key + if not llm_api_key and authorization: + if authorization.startswith("Bearer "): + llm_api_key = authorization[7:] + else: + llm_api_key = authorization + + if not llm_api_key: + raise HTTPException(status_code=401, detail="请在请求头中提供 llm-key 或 Authorization 用于 AI 分析") + + # 从消息中提取地址 + address = extract_address_from_message(request.message) + + chain_data = {} + if address: + # 获取余额 + balance_result = await fetch_balance(address, request.chain, scan_key) + if balance_result["success"]: + chain_data["balance"] = balance_result + + # 获取最近交易 + tx_result = await fetch_transactions(address, request.chain, scan_key, 1, 5) + if tx_result["success"]: + chain_data["recent_transactions"] = tx_result["transactions"][:5] + + # 获取代币 + token_result = await fetch_token_balances(address, request.chain, scan_key) + if token_result["success"]: + chain_data["tokens"] = token_result["tokens"][:10] + + # 构建上下文 + if chain_data: + context_parts = [] + if "balance" in chain_data: + b = chain_data["balance"] + context_parts.append(f"地址: {b['address']}\n余额: {b['balance']} {b['symbol']} ({b['chain_name']})") + if "recent_transactions" in chain_data: + context_parts.append(f"最近交易数: {len(chain_data['recent_transactions'])}") + for tx in chain_data["recent_transactions"][:3]: + context_parts.append(f" - {tx['value']} {tx['symbol']} @ {tx['timestamp'][:10]}") + if "tokens" in chain_data: + context_parts.append(f"持有代币种类: {len(chain_data['tokens'])}") + context = "\n".join(context_parts) + else: + context = "未检测到有效的钱包地址,请提供 0x 开头的以太坊地址" + + # 调用 LLM 生成回复 + llm_response = await chat_with_llm(request.message, context, llm_api_key) + + return ChatResponse( + response=llm_response, + data=chain_data if chain_data else {"detected_address": address}, + timestamp=datetime.utcnow().isoformat() + ) + + +@app.get("/chains") +async def list_chains(): + """列出支持的区块链""" + return { + "chains": [ + { + "id": chain_id, + "name": config["name"], + "symbol": config["symbol"], + "explorer": config["explorer_url"] + } + for chain_id, config in CHAIN_CONFIGS.items() + ] + } + + +# ==================== 主入口 ==================== + +def main(): + """主函数""" + logger.info(f"启动 Chain Explorer Agent - {POD_NAME}") + logger.info(f"支持的区块链: {list(CHAIN_CONFIGS.keys())}") + logger.info(f"回调功能: {'已启用' if CALLBACK_ENABLED else '未启用'}") + + uvicorn.run(app, host=SERVICE_HOST, port=SERVICE_PORT, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/agent_templates/agents/chain_explorer_agent/requirements.txt b/agent_templates/agents/chain_explorer_agent/requirements.txt new file mode 100644 index 0000000..d508938 --- /dev/null +++ b/agent_templates/agents/chain_explorer_agent/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.104.0 +uvicorn>=0.24.0 +aiohttp>=3.9.0 +pydantic>=2.0.0 +python-multipart>=0.0.6 +httpx>=0.25.0 diff --git a/docs/CHAIN_AGENTS_DOC.md b/docs/CHAIN_AGENTS_DOC.md new file mode 100644 index 0000000..a4ff499 --- /dev/null +++ b/docs/CHAIN_AGENTS_DOC.md @@ -0,0 +1,646 @@ +# 链上数据分析 AI Agent 文档 + +--- + +本文档详细介绍了两个链上数据分析 Agent 的功能、API 接口和使用方法。 + +## 概述 + +| Agent | 功能 | 端口 | +|-------|------|------| +| Chain Explorer Agent | 链上数据查询 - 余额、交易、代币 | 8000 | +| Chain Analysis Agent | 链上数据分析 - 活动分析、交易模式、资金流向 | 8000 | + +## 支持的区块链 + +| 网络 | Chain ID | 符号 | 说明 | +|------|----------|------|------| +| Ethereum | ethereum | ETH | 以太坊主网 | +| BSC | bsc | BNB | 币安智能链 | +| Polygon | polygon | POL | Polygon 网络 | +| Arbitrum | arbitrum | ETH | Arbitrum L2 | +| Optimism | optimism | ETH | Optimism L2 | +| Base | base | ETH | Coinbase L2 | + +--- + +## 认证方式 + +所有 API 调用都需要通过请求头传递 API Key: + +| Header | 说明 | 必需 | +|--------|------|------| +| `etherscan-key` | Etherscan API Key(区块链浏览器) | ✅ | +| `api-key` | 备选的区块链浏览器 API Key | ⭕ | +| `llm-key` | LLM API Key(用于 Chat 功能) | Chat 时必需 | +| `Authorization` | Bearer Token(LLM API Key) | Chat 时备选 | + +### 示例 + +```bash +curl -X POST "http://agent-url/balance" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: YOUR_ETHERSCAN_API_KEY" \ + -d '{"address": "0x...", "chain": "ethereum"}' +``` + +--- + +# 1. Chain Explorer Agent - 链上数据查询 + +## 功能概览 + +| 端点 | 方法 | 功能 | +|------|------|------| +| `/` | GET | 服务状态 | +| `/health` | GET | 健康检查 | +| `/chains` | GET | 支持的区块链列表 | +| `/balance` | POST | 查询地址余额 | +| `/transactions` | POST | 查询交易记录 | +| `/tokens` | POST | 查询代币信息 | +| `/chat` | POST | 智能对话 | + +--- + +## 1.1 查询地址余额 + +### 请求 + +```bash +POST /balance +Content-Type: application/json +etherscan-key: YOUR_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| address | string | ✅ | - | 钱包地址 (0x开头) | +| chain | string | ❌ | ethereum | 区块链网络 | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/balance" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -d '{ + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum" + }' +``` + +### 响应 + +```json +{ + "success": true, + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum", + "chain_name": "Ethereum", + "balance_wei": "32116130289281011210", + "balance": 32.11613029, + "symbol": "ETH", + "explorer_url": "https://etherscan.io/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "timestamp": "2026-02-05T17:00:28.539769" +} +``` + +--- + +## 1.2 查询交易记录 + +### 请求 + +```bash +POST /transactions +Content-Type: application/json +etherscan-key: YOUR_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| address | string | ✅ | - | 钱包地址 | +| chain | string | ❌ | ethereum | 区块链网络 | +| page | int | ❌ | 1 | 页码 | +| limit | int | ❌ | 10 | 每页数量 (1-100) | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/transactions" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -d '{ + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum", + "limit": 5 + }' +``` + +### 响应 + +```json +{ + "success": true, + "address": "0x...", + "chain": "ethereum", + "transactions": [ + { + "hash": "0x5b0d81bab...", + "block": "21780123", + "timestamp": "2026-02-05T13:43:47", + "from": "0x...", + "to": "0x...", + "value": 0.000505, + "symbol": "ETH", + "gas_used": "21000", + "gas_price": "5000000000", + "is_error": false, + "tx_url": "https://etherscan.io/tx/0x..." + } + ], + "count": 5, + "page": 1, + "timestamp": "2026-02-05T17:00:30.123456" +} +``` + +--- + +## 1.3 查询代币信息 + +### 请求 + +```bash +POST /tokens +Content-Type: application/json +etherscan-key: YOUR_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| address | string | ✅ | - | 钱包地址 | +| chain | string | ❌ | ethereum | 区块链网络 | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/tokens" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -d '{ + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum" + }' +``` + +### 响应 + +```json +{ + "success": true, + "address": "0x...", + "chain": "ethereum", + "tokens": [ + { + "contract": "0x...", + "name": "Dogelon", + "symbol": "ELON", + "decimals": 18, + "tx_count": 5 + } + ], + "token_count": 49, + "timestamp": "2026-02-05T17:00:35.123456" +} +``` + +--- + +## 1.4 智能对话 (Chat) + +### 请求 + +```bash +POST /chat +Content-Type: application/json +etherscan-key: YOUR_ETHERSCAN_KEY +llm-key: YOUR_LLM_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| message | string | ✅ | - | 用户消息(包含地址) | +| chain | string | ❌ | ethereum | 默认区块链网络 | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/chat" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -H "llm-key: sk-xxx" \ + -d '{ + "message": "帮我查看 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 的余额和交易", + "chain": "ethereum" + }' +``` + +### 响应 + +```json +{ + "response": "地址 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 目前的余额为 32.12 ETH...", + "data": { + "balance": { ... }, + "recent_transactions": [ ... ], + "tokens": [ ... ] + }, + "timestamp": "2026-02-05T17:01:00.123456" +} +``` + +--- + +# 2. Chain Analysis Agent - 链上数据分析 + +## 功能概览 + +| 端点 | 方法 | 功能 | +|------|------|------| +| `/` | GET | 服务状态 | +| `/health` | GET | 健康检查 | +| `/chains` | GET | 支持的区块链列表 | +| `/address-analysis` | POST | 地址活动分析 | +| `/transaction-patterns` | POST | 交易模式分析 | +| `/fund-flow` | POST | 资金流向分析 | +| `/contract-interactions` | POST | 合约交互分析 | +| `/chat` | POST | 智能分析对话 | + +--- + +## 2.1 地址活动分析 + +分析指定时间段内的地址活动,包括收支统计、活跃度等。 + +### 请求 + +```bash +POST /address-analysis +Content-Type: application/json +etherscan-key: YOUR_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| address | string | ✅ | - | 钱包地址 | +| chain | string | ❌ | ethereum | 区块链网络 | +| days | int | ❌ | 30 | 分析天数 (1-365) | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/address-analysis" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -d '{ + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum", + "days": 30 + }' +``` + +### 响应 + +```json +{ + "address": "0x...", + "chain": "ethereum", + "period_days": 30, + "summary": { + "total_sent": 1.0, + "total_received": 0.004591, + "net_flow": -0.995409, + "tx_count_in": 52, + "tx_count_out": 11, + "total_tx": 63, + "failed_tx": 2, + "unique_addresses": 31, + "active_days": 18 + }, + "symbol": "ETH", + "current_balance": 32.11613029, + "daily_activity": { ... }, + "timestamp": "2026-02-05T17:02:00.123456" +} +``` + +--- + +## 2.2 交易模式分析 + +分析地址的交易行为模式,包括时间分布、金额分布、高频交互对手等。 + +### 请求 + +```bash +POST /transaction-patterns +Content-Type: application/json +etherscan-key: YOUR_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| address | string | ✅ | - | 钱包地址 | +| chain | string | ❌ | ethereum | 区块链网络 | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/transaction-patterns" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -d '{ + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum" + }' +``` + +### 响应 + +```json +{ + "address": "0x...", + "chain": "ethereum", + "patterns": { + "hourly_distribution": { "0": 5, "14": 20, ... }, + "daily_distribution": { "Monday": 10, "Tuesday": 15, ... }, + "value_distribution": { + "micro": 198, // < 0.01 ETH + "small": 1, // 0.01 - 0.1 ETH + "medium": 0, // 0.1 - 1 ETH + "large": 1, // 1 - 10 ETH + "whale": 0 // > 10 ETH + }, + "avg_interval_hours": 12.5, + "top_counterparties": [ + { "address": "0x...", "tx_count": 15 } + ] + }, + "behavior_summary": "活跃高峰时段: 14:00 UTC; 以小额交易为主(可能是频繁交易者或机器人)", + "timestamp": "2026-02-05T17:02:30.123456" +} +``` + +--- + +## 2.3 资金流向分析 + +分析资金来源和去向,识别主要入金/出金地址。 + +### 请求 + +```bash +POST /fund-flow +Content-Type: application/json +etherscan-key: YOUR_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| address | string | ✅ | - | 钱包地址 | +| chain | string | ❌ | ethereum | 区块链网络 | +| limit | int | ❌ | 100 | 分析交易数量 (10-500) | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/fund-flow" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -d '{ + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum", + "limit": 100 + }' +``` + +### 响应 + +```json +{ + "address": "0x...", + "chain": "ethereum", + "fund_flow": { + "total_inflow": 5.234, + "total_outflow": 3.156, + "net_flow": 2.078, + "inflow_sources": 42, + "outflow_destinations": 8, + "top_inflow": [ + { "address": "0x...", "amount": 2.5, "symbol": "ETH" } + ], + "top_outflow": [ + { "address": "0x...", "amount": 1.0, "symbol": "ETH" } + ] + }, + "symbol": "ETH", + "timestamp": "2026-02-05T17:03:00.123456" +} +``` + +--- + +## 2.4 合约交互分析 + +分析地址与智能合约的交互情况。 + +### 请求 + +```bash +POST /contract-interactions +Content-Type: application/json +etherscan-key: YOUR_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| address | string | ✅ | - | 钱包地址 | +| chain | string | ❌ | ethereum | 区块链网络 | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/contract-interactions" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -d '{ + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "chain": "ethereum" + }' +``` + +### 响应 + +```json +{ + "address": "0x...", + "chain": "ethereum", + "contract_interactions": { + "total_contracts": 10, + "top_contracts": [ + { + "contract": "0x...", + "interaction_count": 9, + "unique_methods": 1, + "total_value": 0.5, + "symbol": "ETH", + "explorer_url": "https://etherscan.io/address/0x..." + } + ] + }, + "timestamp": "2026-02-05T17:03:30.123456" +} +``` + +--- + +## 2.5 智能分析对话 (Chat) + +### 请求 + +```bash +POST /chat +Content-Type: application/json +etherscan-key: YOUR_ETHERSCAN_KEY +llm-key: YOUR_LLM_API_KEY +``` + +### 参数 + +| 参数 | 类型 | 必需 | 默认值 | 说明 | +|------|------|------|--------|------| +| message | string | ✅ | - | 分析请求(包含地址) | +| chain | string | ❌ | ethereum | 默认区块链网络 | + +### 示例 + +```bash +curl -X POST "http://localhost:8000/chat" \ + -H "Content-Type: application/json" \ + -H "etherscan-key: F71AZ6XW2WN6GK3D63HJ7AVAEDC9M42EZY" \ + -H "llm-key: sk-xxx" \ + -d '{ + "message": "分析 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 是不是巨鲸或机器人", + "chain": "ethereum" + }' +``` + +### 响应 + +```json +{ + "response": "### 地址分析报告\n\n#### 一、基本信息\n- **当前余额**: 32.12 ETH\n...", + "analysis": { + "activity": { ... }, + "patterns": { ... }, + "fund_flow": { ... }, + "contracts": { ... }, + "balance": 32.11613029 + }, + "timestamp": "2026-02-05T17:04:00.123456" +} +``` + +--- + +## 统一错误格式 + +### 成功响应 + +```json +{ + "success": true, + "data": { ... }, + "timestamp": "2026-02-05T17:00:00.000000" +} +``` + +### 错误响应 + +```json +{ + "detail": "错误信息描述" +} +``` + +### HTTP 状态码 + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功 | +| 400 | 请求参数错误 | +| 401 | 未提供 API Key | +| 404 | 未找到数据 | +| 500 | 服务器错误 | + +--- + +## 部署信息 + +| Agent | 镜像地址 | 端口 | +|-------|----------|------| +| Chain Explorer | `agnettaiji.azurecr.io/ai-agents/chain-explorer-agent:latest` | 8000 | +| Chain Analysis | `agnettaiji.azurecr.io/ai-agents/chain-analysis-agent:latest` | 8000 | + +### 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `SERVICE_HOST` | 0.0.0.0 | 服务绑定地址 | +| `SERVICE_PORT` | 8000 | 服务端口 | +| `LLM_BASE_URL` | https://litellm.xxx | LLM 服务地址 | +| `LLM_MODEL` | taiji/gpt-4o-mini | LLM 模型 | + +--- + +## 测试用地址 + +| 地址 | 说明 | +|------|------| +| `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` | Vitalik Buterin | +| `0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8` | Binance Cold Wallet | +| `0x28C6c06298d514Db089934071355E5743bf21d60` | Binance Hot Wallet | + +--- + +## 最佳实践 + +1. **API Key 管理**:Etherscan API 有速率限制,建议申请付费 API Key +2. **缓存策略**:对于不常变化的数据(如历史交易),建议本地缓存 +3. **并发控制**:避免短时间内大量请求,建议间隔 200ms +4. **多链支持**:使用统一的 Etherscan V2 API,通过 chainid 区分网络 + +--- + +## 版本历史 + +| 版本 | 日期 | 更新内容 | +|------|------|----------| +| 1.0.0 | 2026-02-05 | 初始版本,支持 Etherscan V2 API |