diff --git a/.gitignore b/.gitignore index 96fab4f..1e96a8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,32 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# ============================================================================== +# 🟢 General / OS / IDE +# ============================================================================== +# Mac OS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# IDEs (VS Code, JetBrains) +.idea/ +.vscode/ +*.swp +*.swo +# ============================================================================== +# 🟡 Node.js / Next.js (Frontend) +# ============================================================================== # Dependencies node_modules .pnp .pnp.js -# Local env files -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - # Testing coverage -# Turbo +# Turbo Repo .turbo # Vercel @@ -27,12 +38,82 @@ out/ build dist - -# Debug +# Debug Logs npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* -# Misc -.DS_Store +# ============================================================================== +# 🔵 Python / FastAPI (Backend) +# ============================================================================== +# Byte-compiled / Optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / Packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual Environments (非常重要,不要上傳 venv) +.venv/ +venv/ +ENV/ +env/ + +# Unit Test / Coverage +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Type Checking +.mypy_cache/ +.dmypy.json +dmypy.json + +# ============================================================================== +# 🔴 Secrets & Local Data (Privacy Protection) +# ============================================================================== +# Local env files (金鑰與設定) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.env*.local + +# SSL Certificates *.pem +*.key + +# User Uploads (模塊一:交易紀錄) +# 忽略 server 下的 uploads 資料夾,保護用戶隱私 +apps/server/uploads/ +**/uploads/ \ No newline at end of file diff --git a/apps/server/main.py b/apps/server/main.py index c685b0e..74bb544 100644 --- a/apps/server/main.py +++ b/apps/server/main.py @@ -1,3 +1,46 @@ -from spoonos_server.server.app import app +import os +import shutil +from pathlib import Path +from fastapi import UploadFile, File +from spoonos_server.server.app import app # 保持原本的導入,這是主程式 -__all__ = ["app"] +# --- [模塊一核心] 檔案上傳基礎設施 --- + +# 1. 設定上傳目錄 +# 這會在 apps/server/ 下自動建立一個 'uploads' 資料夾 +UPLOAD_DIR = Path(__file__).parent / "uploads" +UPLOAD_DIR.mkdir(exist_ok=True) + +@app.post("/api/upload") +async def upload_file(file: UploadFile = File(...)): + """ + [Investment Profiler 專用接口] + 接收前端上傳的交易文件 (CSV/Excel),存到伺服器本地,並返回絕對路徑。 + """ + try: + # 為了防止檔名重複,你也可以在這裡加上時間戳記 (uuid) + # 目前保持原檔名,方便你測試 + safe_filename = os.path.basename(file.filename) + file_location = UPLOAD_DIR / safe_filename + + # 2. 將上傳的文件寫入磁碟 (Stream 寫入,節省記憶體) + with open(file_location, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # 3. 獲取絕對路徑 (這是 Python Tool 讀取所需的關鍵參數) + absolute_path = str(file_location.resolve()) + + print(f"✅ [Upload] 文件已保存至: {absolute_path}") + + return { + "status": "success", + "file_path": absolute_path, # <--- 前端拿到這個路徑後,要塞進 Prompt 裡 + "filename": safe_filename, + "message": "上傳成功!請將 file_path 提供給 Agent 進行分析。" + } + except Exception as e: + print(f"❌ [Upload Error] {str(e)}") + return {"status": "error", "message": str(e)} + +# 導出 app 供 uvicorn 啟動 +__all__ = ["app"] \ No newline at end of file diff --git a/apps/server/requirements.txt b/apps/server/requirements.txt index 9d05d06..9256247 100644 --- a/apps/server/requirements.txt +++ b/apps/server/requirements.txt @@ -146,10 +146,12 @@ x402>=0.2.1,<1.0 # Extra dependencies merged from the separate Crypto PowerData requirements.txt pandas>=1.0.0 +openpyxl>=3.1.5 # Crypto PowerData dependencies ccxt>=4.0.0 numpy>=1.20.0 +quantstats>=0.0.81 TA-Lib>=0.4.25 asyncio-throttle>=1.0.0 pydantic-settings>=2.0.0 @@ -159,5 +161,10 @@ gql>=3.5.0 # Development dependencies pytest>=7.0.0 pytest-cov>=4.0.0 +pytest-asyncio>=0.21.0 bump2version>=1.0.0 pycares>=4.9,<5.0 + +# FastAPI file upload support +python-multipart>=0.0.9 + diff --git a/apps/server/spoonos_server/skills/investment_profiler/SKILL.md b/apps/server/spoonos_server/skills/investment_profiler/SKILL.md new file mode 100644 index 0000000..b5a3caa --- /dev/null +++ b/apps/server/spoonos_server/skills/investment_profiler/SKILL.md @@ -0,0 +1,7 @@ +# Investment Profiler + +## Description +This skill analyzes user transaction history (CSV) and psychological questionnaire results to generate a personalized MBTI investment personality profile. + +## Tools +- `analyze_investment_personality`: Takes a CSV file string and a questionnaire JSON, returns a comprehensive analysis report. \ No newline at end of file diff --git a/apps/server/spoonos_server/skills/investment_profiler/__init__.py b/apps/server/spoonos_server/skills/investment_profiler/__init__.py new file mode 100644 index 0000000..1f0f9e0 --- /dev/null +++ b/apps/server/spoonos_server/skills/investment_profiler/__init__.py @@ -0,0 +1,5 @@ +# 這裡必須跟 tools.py 裡的函數名稱完全一致 +from .tools import analyze_user_profile + +# 導出工具列表 +tools = [analyze_user_profile] \ No newline at end of file diff --git a/apps/server/spoonos_server/skills/investment_profiler/core/performance.py b/apps/server/spoonos_server/skills/investment_profiler/core/performance.py new file mode 100644 index 0000000..06a4aa5 --- /dev/null +++ b/apps/server/spoonos_server/skills/investment_profiler/core/performance.py @@ -0,0 +1,113 @@ +import pandas as pd +import quantstats as qs +import numpy as np +import os + +class PerformanceEngine: + def process_trade_history(self, file_path: str) -> dict: + """ + 全能型交易文件讀取器:支持 Excel, 標準 CSV, 以及帶雜訊 Header 的 CSV + """ + df = pd.DataFrame() + try: + # --- 階段 1: 智能讀取 (Smart Ingestion) --- + if file_path.endswith(('.xlsx', '.xls')): + try: + df = pd.read_excel(file_path) + except Exception as e: + return {"error": f"Excel 讀取失敗: {str(e)}"} + else: + # CSV 處理邏輯 + encodings = ['utf-8', 'utf-8-sig', 'big5', 'gbk'] + success = False + + for enc in encodings: + try: + # 策略 A: 先讀第一行看看長什麼樣 + # header=None 代表先不要把第一行當標題 + preview = pd.read_csv(file_path, encoding=enc, nrows=2, header=None) + first_cell = str(preview.iloc[0, 0]) + + # 檢測 AssetChangeDetails 格式 (特徵: 第一行開頭是 UID:) + if "UID:" in first_cell or "Company Name" in first_cell: + # 策略 B: 跳過雜訊行 (skiprows=1) + df = pd.read_csv(file_path, encoding=enc, skiprows=1) + else: + # 策略 C: 標準讀取 + df = pd.read_csv(file_path, encoding=enc) + + if not df.empty: + success = True + break + except: + continue + + if not success: + return {"error": "無法識別文件編碼,請嘗試轉存為 UTF-8 CSV"} + + # --- 階段 2: 欄位識別 (Column Mapping) --- + if df.empty: + return {"error": "文件為空"} + + # 清洗欄位名稱 (去除空格) + df.columns = [str(c).strip() for c in df.columns] + + # 定義我們的「字典」,讓程式能看懂不同交易所的語言 + col_map = { + 'time': ['Time(UTC)', 'Time', 'Date', '開倉時間', 'Created Time', '交易時間', 'datetime'], + 'profit': ['Change', 'Profit', 'Realized PNL', '已實現收益', 'Amount', 'PNL', 'Cash Flow'] + } + + target_cols = {} + for key, candidates in col_map.items(): + for col in candidates: + if col in df.columns: + target_cols[key] = col + break + + if 'time' not in target_cols or 'profit' not in target_cols: + return {"error": f"欄位識別失敗。檢測到的欄位: {list(df.columns)}"} + + # --- 階段 3: 數據計算 (Quant Analysis) --- + # 轉換時間 + df['Time'] = pd.to_datetime(df[target_cols['time']]) + df.set_index('Time', inplace=True) + df.sort_index(inplace=True) + + # 提取損益序列 + pnl_series = df[target_cols['profit']] + + # 數據清洗:把非數字轉為 0 (例如 'TRANSFER' 這種文字) + pnl_series = pd.to_numeric(pnl_series, errors='coerce').fillna(0) + + # 估算本金 (用於計算收益率 %) + # 邏輯:如果總流水很大,假設本金是流水的 10%;否則假設固定 1000U + total_flow = pnl_series.abs().sum() + initial_capital = total_flow * 0.1 if total_flow > 0 else 1000 + returns = pnl_series / initial_capital + + # 使用 QuantStats + metrics = {} + try: + # 只有當數據點足夠多時才算夏普率 + if len(returns) > 5: + metrics = { + "sharpe_ratio": qs.stats.sharpe(returns), + "win_rate": qs.stats.win_rate(returns), + "max_drawdown": qs.stats.max_drawdown(returns), + "profit_factor": qs.stats.profit_factor(returns), + "total_trades": len(df), + "total_pnl_value": pnl_series.sum() + } + else: + metrics = {"total_trades": len(df), "note": "數據過少"} + except: + metrics = {"error": "QuantStats 計算異常", "total_trades": len(df)} + + # 轉為 Python 原生類型 (JSON Friendly) + return {k: float(v) if isinstance(v, (np.float64, np.float32)) else v for k, v in metrics.items()} + + except Exception as e: + import traceback + traceback.print_exc() + return {"error": f"分析崩潰: {str(e)}"} \ No newline at end of file diff --git a/apps/server/spoonos_server/skills/investment_profiler/core/questionnaire.py b/apps/server/spoonos_server/skills/investment_profiler/core/questionnaire.py new file mode 100644 index 0000000..9e0ebaf --- /dev/null +++ b/apps/server/spoonos_server/skills/investment_profiler/core/questionnaire.py @@ -0,0 +1,63 @@ +#算問卷的 +import re + +class QuestionnaireEngine: + def process_answers(self, answers: dict) -> dict: + """ + 對用戶的 MBTI 問卷回答進行計分與分析。 + 兼容格式: + - {"EI_01": "A"} + - {"EI_01": "A: 選項內容..."} + - {"EI_01": "選項A"} + """ + # 初始化計分板 + scores = {"E": 0, "I": 0, "S": 0, "N": 0, "T": 0, "F": 0, "J": 0, "P": 0} + + raw_text_list = [] + + for q_id, answer_text in answers.items(): + ans_str = str(answer_text).strip().upper() + raw_text_list.append(f"- {q_id}: {ans_str}") + + # --- 核心容錯邏輯 --- + # 只要字串開頭是 B,或是包含 "選項B",就算 B + # 否則默認算 A (因為題目結構是二選一,A通常在前面) + choice = "A" + if ans_str.startswith("B") or "選項B" in ans_str or "(B)" in ans_str: + choice = "B" + + # 解析維度 (從 ID "EI_01" 解析出 "EI") + if "_" in q_id: + dim_key = q_id.split("_")[0] # 拿到 "EI", "SN"... + + if len(dim_key) == 2: + left_char = dim_key[0] # E, S, T, J + right_char = dim_key[1] # I, N, F, P + + if choice == "A": + scores[left_char] += 1 + else: + scores[right_char] += 1 + + # 結算與生成報告 (保持不變) + mbti_result = "" + mbti_result += "E" if scores["E"] >= scores["I"] else "I" + mbti_result += "S" if scores["S"] >= scores["N"] else "N" + mbti_result += "T" if scores["T"] >= scores["F"] else "F" + mbti_result += "J" if scores["J"] >= scores["P"] else "P" + + analysis_report = ( + f"【問卷計分結果】\n" + f"- E/I: {scores['E']}/{scores['I']}\n" + f"- S/N: {scores['S']}/{scores['N']}\n" + f"- T/F: {scores['T']}/{scores['F']}\n" + f"- J/P: {scores['J']}/{scores['P']}\n" + f"👉 綜合自述類型: {mbti_result}" + ) + + return { + "mbti_type": mbti_result, + "scores": scores, + "analysis_text": analysis_report, + "raw_text": "\n".join(raw_text_list) + } \ No newline at end of file diff --git a/apps/server/spoonos_server/skills/investment_profiler/data/question_bank.py b/apps/server/spoonos_server/skills/investment_profiler/data/question_bank.py new file mode 100644 index 0000000..dfba4aa --- /dev/null +++ b/apps/server/spoonos_server/skills/investment_profiler/data/question_bank.py @@ -0,0 +1,169 @@ +# 這裡定義 16 道題目的資料結構 +# 注意:選項 A 預設對應維度的第一個字母 (左),選項 B 對應第二個字母 (右) +# 例如 dimension: "EI" -> A=E, B=I + +MBTI_QUESTIONS = [ + # ========================================================================= + # 1. E/I 維度 (外向/內向) + # A -> E (外向), B -> I (內向) + # ========================================================================= + { + "id": "EI_01", + "dimension": "EI", + "question": "在交易群組或社群中,你的活躍程度是?", + "options": { + "A": "非常活躍,喜歡分享觀點並與他人討論", + "B": "潛水居多,喜歡自己默默消化資訊" + } + }, + { + "id": "EI_02", + "dimension": "EI", + "question": "連續虧損心情不好時,你傾向於?", + "options": { + "A": "找幣圈朋友訴苦、吐槽或討論哪裡做錯了", + "B": "關掉電腦,一個人靜靜思考、復盤或散心" + } + }, + { + "id": "EI_03", + "dimension": "EI", + "question": "看到一個潛在的暴漲機會,你通常會?", + "options": { + "A": "馬上發到群組問大家怎麼看,尋求認同", + "B": "先自己查資料驗證,確認無誤後再考慮進場" + } + }, + { + "id": "EI_04", + "dimension": "EI", + "question": "你認為交易獲利的最大樂趣在於?", + "options": { + "A": "向別人展示績效截圖,獲得稱讚與成就感", + "B": "看著帳戶數字增長,享受個人策略驗證成功的快感" + } + }, + + # ========================================================================= + # 2. S/N 維度 (實感/直覺) + # A -> S (實感), B -> N (直覺) + # ========================================================================= + { + "id": "SN_01", + "dimension": "SN", + "question": "你更看重哪種分析依據?", + "options": { + "A": "具體的 K 線型態、成交量與歷史數據", + "B": "項目的未來願景、賽道潛力與敘事邏輯" + } + }, + { + "id": "SN_02", + "dimension": "SN", + "question": "對於一個新發行的代幣 (Token),你更關注?", + "options": { + "A": "當前的流通市值、解鎖時間表和鏈上數據", + "B": "白皮書描繪的宏大藍圖和它能解決什麼未來問題" + } + }, + { + "id": "SN_03", + "dimension": "SN", + "question": "你的交易筆記或復盤內容通常是?", + "options": { + "A": "詳細記錄進出場點位、損益金額和操作細節", + "B": "記錄當時的市場情緒、大局觀判斷和靈感想法" + } + }, + { + "id": "SN_04", + "dimension": "SN", + "question": "當市場出現突發新聞時,你傾向於?", + "options": { + "A": "確認該新聞對價格的實際影響幅度後再行動", + "B": "憑直覺判斷這是否會改變長期趨勢,迅速反應" + } + }, + + # ========================================================================= + # 3. T/F 維度 (思考/情感) + # A -> T (邏輯), B -> F (情感) + # ========================================================================= + { + "id": "TF_01", + "dimension": "TF", + "question": "面對持倉暴跌 20%,你的第一反應是?", + "options": { + "A": "檢查是否觸發止損條件,按紀律執行賣出", + "B": "感到焦慮,但相信這只是錯殺,選擇繼續持有" + } + }, + { + "id": "TF_02", + "dimension": "TF", + "question": "你決定賣出一個幣的主要原因是?", + "options": { + "A": "技術指標出現賣訊或基本面數據變差", + "B": "感覺漲不動了,或者對這個項目失去了信心" + } + }, + { + "id": "TF_03", + "dimension": "TF", + "question": "如果有朋友推薦你買一支你也看好的幣,你會?", + "options": { + "A": "依然按照自己的分析系統,計算好盈虧比再進場", + "B": "基於信任和共同話題,會比平常更果斷地買入" + } + }, + { + "id": "TF_04", + "dimension": "TF", + "question": "回顧一筆虧損的交易,你會如何評價自己?", + "options": { + "A": "客觀分析策略漏洞,思考下次如何優化邏輯", + "B": "感到自責或懊惱,覺得自己運氣不好或心態炸裂" + } + }, + + # ========================================================================= + # 4. J/P 維度 (判斷/感知) + # A -> J (計畫), B -> P (彈性) + # ========================================================================= + { + "id": "JP_01", + "dimension": "JP", + "question": "你的交易計畫通常是?", + "options": { + "A": "進場前就寫好止盈止損點位,嚴格執行", + "B": "有個大概方向,具體操作看盤面感覺隨機應變" + } + }, + { + "id": "JP_02", + "dimension": "JP", + "question": "對於每天的看盤時間,你習慣?", + "options": { + "A": "有固定的時間段 (例如開盤前、收盤後) 進行分析", + "B": "想看就看,或者收到行情提醒時才打開 APP" + } + }, + { + "id": "JP_03", + "dimension": "JP", + "question": "當你的交易策略連續失效時,你會?", + "options": { + "A": "暫停交易,重新審視並修訂整套規則", + "B": "嘗試換一種新的玩法或指標,看看能不能轉運" + } + }, + { + "id": "JP_04", + "dimension": "JP", + "question": "你更喜歡哪種交易風格?", + "options": { + "A": "穩健的波段策略,每一步都在掌控之中", + "B": "刺激的短線衝刺,享受捕捉瞬間行情的快感" + } + } +] \ No newline at end of file diff --git a/apps/server/spoonos_server/skills/investment_profiler/tools.py b/apps/server/spoonos_server/skills/investment_profiler/tools.py new file mode 100644 index 0000000..3db6514 --- /dev/null +++ b/apps/server/spoonos_server/skills/investment_profiler/tools.py @@ -0,0 +1,132 @@ +import logging +import random +import json +from spoonos_server.tools import tool +from .core.performance import PerformanceEngine +from .core.questionnaire import QuestionnaireEngine +from .data.question_bank import MBTI_QUESTIONS # 導入題庫 + +logger = logging.getLogger(__name__) + +# ============================================================================== +# 工具 1: 生成隨機問卷 +# ============================================================================== +@tool +def generate_investment_quiz() -> str: + """ + 從題庫中為每個 MBTI 維度 (E/I, S/N, T/F, J/P) 隨機抽取 2 道題目,共 8 題。 + Agent 應在對話初期使用此工具,獲取題目並呈現給用戶回答。 + + Returns: + str: 包含 8 道題目的 JSON 格式字串。 + """ + logger.info("正在生成隨機投資問卷 (8題)...") + + try: + # 1. 將題目按維度分類 + categories = {"EI": [], "SN": [], "TF": [], "JP": []} + for q in MBTI_QUESTIONS: + dim = q.get("dimension") + if dim in categories: + categories[dim].append(q) + + # 2. 每個維度隨機抽 2 題 + selected_quiz = [] + for dim, questions in categories.items(): + # 使用 min 確保即使題庫不足 2 題也不會報錯 + count = min(len(questions), 2) + if count > 0: + selected = random.sample(questions, count) + selected_quiz.extend(selected) + + # 3. 返回 JSON 供 Agent 讀取 + return json.dumps(selected_quiz, ensure_ascii=False, indent=2) + + except Exception as e: + logger.error(f"生成問卷失敗: {e}") + return json.dumps({"error": "無法生成題目,請檢查題庫配置。"}) + + +# ============================================================================== +# 工具 2: 雙重人格分析 (行為數據 + 問卷計分) +# ============================================================================== +@tool +def analyze_user_profile(file_path: str, questionnaire: dict) -> str: + """ + [模塊一核心] 綜合分析用戶的「交易歷史(行為)」與「問卷(自述)」,推導 MBTI 投資人格。 + + Args: + file_path: 交易文件路徑 (Excel/CSV)。 + questionnaire: 用戶的問卷回答字典 (格式: {"題目ID": "用戶選擇的完整選項字串或代號"}). + """ + logger.info(f"正在執行雙重人格分析... 路徑: {file_path}") + + # --- 1. 硬數據分析 (Python PerformanceEngine) --- + perf_engine = PerformanceEngine() + metrics = {} + try: + metrics = perf_engine.process_trade_history(file_path) + # 如果返回的是錯誤字典 + if "error" in metrics: + return f"❌ 交易數據分析錯誤: {metrics['error']}" + except Exception as e: + return f"❌ 交易文件讀取失敗: {str(e)}" + + # --- 2. 軟數據處理 (Python QuestionnaireEngine 計分) --- + # 這裡不再只是轉文字,而是進行邏輯計分 (E vs I, T vs F...) + quiz_engine = QuestionnaireEngine() + soft_data = {} + try: + # 預期 process_answers 會返回包含 'mbti_type', 'scores', 'analysis_text' 的字典 + soft_data = quiz_engine.process_answers(questionnaire) + except Exception as e: + logger.error(f"問卷計分失敗: {e}") + soft_data = { + "analysis_text": "❌ 問卷計分發生錯誤,請依賴交易數據進行分析。", + "mbti_type": "Unknown" + } + + # --- 3. 構建「雙重驗證」Prompt --- + + return f""" + === 🕵️‍♂️ 投資人格雙重分析請求 === + + 請根據以下兩組數據 (行為 vs 自述),推導用戶的 MBTI 投資人格。 + + 【數據組 A:真實交易行為 (Behavioral Persona)】 + (這是用戶實際做出來的,反映潛意識與執行力,權重較高) + - 總交易次數: {metrics.get('total_trades', 0)} (高頻->E / 低頻->I) + - 勝率 (Win Rate): {metrics.get('win_rate', 0):.2%} + - 盈虧比 (Profit Factor): {metrics.get('profit_factor', 0)} + - 夏普比率 (Sharpe): {metrics.get('sharpe_ratio', 0)} (高->T/J 紀律強 / 低->F/P 情緒化) + - 最大回撤 (MDD): {metrics.get('max_drawdown', 0):.2%} (大回撤->可能為 P 或 F) + + 【數據組 B:問卷自述 (Self-Reported Persona)】 + (這是系統根據用戶回答,自動運算出的計分結果) + {soft_data.get('analysis_text', '無有效問卷數據')} + + === 🧠 分析指令 (CoT) === + 請依序執行以下思考步驟,不要跳過: + + 1. **推導行為 MBTI**:僅根據【數據組 A】,判斷用戶像是什麼類型?(例如:數據顯示頻繁交易且回撤大,行為像 ESTP)。 + 2. **確認自述 MBTI**:參考【數據組 B】的計分結果 (系統計算為 {soft_data.get('mbti_type', '未知')})。 + 3. **衝突檢測 (關鍵)**: + - 如果 A 與 B 一致(例如都是 ESTP):確認為該類型,並讚賞用戶知行合一。 + - 如果 A 與 B 衝突(例如行為是賭徒 ESTP,自述是專家 ISTJ):**以 A (行為) 為主**。請指出用戶存在「知行不一」的問題,可能是因為執行力不足或對自己有誤解。 + + === 📝 最終輸出格式 === + (請直接輸出以下 Markdown 格式) + + ## 🎯 你的投資人格:[最終 MBTI 代碼] - [稱號] + + ### 📊 雙重驗證分析 + - **行為顯示 (數據)**:傾向 [行為MBTI],因為你的數據顯示... + - **你認為自己 (問卷)**:傾向 [自述MBTI],根據問卷計分... + + ### 💡 深度洞察 + (請在此處詳細分析兩者的落差或一致性,並給出心理學解釋) + + ### 🚀 給 [最終MBTI] 的進化建議 + 1. ... + 2. ... + """ \ No newline at end of file diff --git a/apps/server/spoonos_server/skills/registry.py b/apps/server/spoonos_server/skills/registry.py index 3a5c517..0f8b136 100644 --- a/apps/server/spoonos_server/skills/registry.py +++ b/apps/server/spoonos_server/skills/registry.py @@ -1,8 +1,10 @@ from pathlib import Path from typing import Dict, List +from spoonos_server.skills import investment_profiler def load_skill_index(root: Path) -> Dict[str, List[str]]: + if not root.exists(): return {} @@ -14,3 +16,8 @@ def load_skill_index(root: Path) -> Dict[str, List[str]]: if skill_file.exists(): skills[skill_dir.name] = [str(skill_file)] return skills + + +SKILL_REGISTRY = [ + investment_profiler, +] \ No newline at end of file diff --git a/apps/server/spoonos_server/tools/__init__.py b/apps/server/spoonos_server/tools/__init__.py index fe954a4..8850246 100644 --- a/apps/server/spoonos_server/tools/__init__.py +++ b/apps/server/spoonos_server/tools/__init__.py @@ -1 +1,9 @@ """Toolkits and tool loaders.""" + +"""Toolkits and tool loaders.""" + +# 從我們剛建立的 decorator.py 導入 tool +from .decorator import tool + +# 定義導出列表,這樣外部就能使用 `from spoonos_server.tools import tool` +__all__ = ["tool"] \ No newline at end of file diff --git a/apps/server/spoonos_server/tools/decorator.py b/apps/server/spoonos_server/tools/decorator.py new file mode 100644 index 0000000..1e84ccf --- /dev/null +++ b/apps/server/spoonos_server/tools/decorator.py @@ -0,0 +1,14 @@ +import functools + +def tool(func): + """ + 一個簡單的裝飾器,用於將函數標記為 Agent 可用的工具。 + 它會保留函數原本的名稱和文檔字符串 (Docstring)。 + """ + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + # 標記這個函數是一個 tool (供註冊表掃描用) + wrapper.is_tool = True + return wrapper \ No newline at end of file