# AI人工智慧導論期末專題 AI agent 期末專題進度: https://hackmd.io/@moolimuli/SkAElaGMbg/edit ## :triangular_flag_on_post: 25/12/07 Update: :::info 1. 頭像圖片自己下載,到 **js** 改頭像檔案 2. 背景圖片 :::spoiler 圖檔 ![back](https://hackmd.io/_uploads/BJLifnMG-x.jpg) ::: 4. 字體載點:https://www.ziti.net.cn/mianfeiziti/955.html - 新增font資料夾,ttf檔案放入 :::spoiler 示意圖 ![Screenshot 2025-12-07 at 15.55.13](https://hackmd.io/_uploads/BJR17hfGbe.png) ::: - ttf檔案更名 **GenWanMinTWTTFSemiBold** 5. 頭像圖檔 :::spoiler 用戶頭像 ![girl](https://hackmd.io/_uploads/rkd_1gVM-x.png) ::: :::spoiler kamisama ![kamisama](https://hackmd.io/_uploads/r170kg4GZl.png) ::: ::: ### html :::spoiler html ```html= <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>AI 小幫手 - 戀愛急診室</title> <link rel="icon" href="data:,"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}"> </head> <body> <header> 戀愛急診室 </header> <div class="scrollable-container" id="dialog-div"> </div> <div class="input-controls"> <input type="text" id="message" name="message" placeholder="輸入你的訊息..." autocomplete="off"> <button id="submit" type="button">傳送</button> </div> <script src="//unpkg.com/jquery"></script> <script src="{{url_for('static', filename='js/main.js')}}"></script> </body> </html> ``` ::: --- ### css :::spoiler css ```css= /* ========================================= 全局樣式設定 ========================================= */ html, body { height: 100%; /* 強制高度為視窗高度 */ margin: 0; padding: 0; overflow: hidden; /* 關鍵:禁止整個網頁出現捲軸 */ } body { font-family: 'Microsoft JhengHei', Arial, sans-serif; display: flex; flex-direction: column; /* 垂直排列:標題 -> 對話框 -> 輸入框 */ align-items: center; /* 背景設定 */ background-image: url('../images/back.jpg'); background-size: cover; background-position: center; background-repeat: no-repeat; /* 讓背景淡入 */ animation: fadeIn 1s ease-out; } body::before { content: ''; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(154, 32, 62, 0.2); z-index: 0; pointer-events: none; mix-blend-mode: multiply; } /* ========================================= 字型載入 ========================================= */ @font-face { font-family: 'GenWanMin'; src: url('../font/GenWanMinTWTTFSemiBold.ttf') format('truetype'); font-style: normal; } /* ========================================= 標題樣式 ========================================= */ header { padding: 30px 0; font-family: 'GenWanMin', serif; font-size: 4rem; font-weight: 600; letter-spacing: 0.15em; z-index: 1; text-shadow: 2px 2px 4px rgba(0,0,0,0.1); color : #73242A; flex-shrink: 0; } /* ========================================= 聊天視窗容器樣式 ========================================= */ /* --- 聊天視窗容器 --- */ .scrollable-container { /* 佈局核心設定 */ flex: 1; /* 關鍵:自動佔滿剩餘空間 */ min-height: 0; /* 關鍵:允許容器縮小,否則內容多時會撐爆 Flex 容器 */ width: 900px; max-width: 95vw; /* 外距調整:上下留一點空間 */ margin: 0px auto 20px auto; padding: 30px; box-sizing: border-box; /* 內部捲動設定 */ overflow-y: auto; /* 內容超出時,只有這裡會捲動 */ overflow-x: hidden; /* 外觀維持不變 */ border: none; border-radius: 20px; background: rgba(255, 255, 255, 0.4); backdrop-filter: blur(25px); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; position: relative; z-index: 1; animation: fadeIn 1.2s ease-out; } /* ========================================= 訊息行樣式 ========================================= */ /* --- 訊息行通用樣式 --- */ .chat-row { display: flex; align-items: flex-start; margin-bottom: 25px; width: 100%; } .chat-row img { width: 55px; /* 稍微加大頭像 */ height: 55px; border-radius: 50%; object-fit: cover; border: 3px solid rgba(233, 177, 188, 0.5); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); flex-shrink: 0; } /* --- 訊息氣泡通用樣式 --- */ .msg-content { padding: 14px 20px; border-radius: 20px; font-size: 16px; line-height: 1.25; position: relative; max-width: calc(100% - 150px); width: fit-content; word-break: break-word; /* white-space: pre-wrap; */ /* 3. 確保文字靠左 */ text-align: left; } /* --- AI (左側) --- */ .ai-row { justify-content: flex-start; } .ai-row .msg-content { margin-left: 18px; margin-top: 10px; background: rgba(240, 227, 206, 0.75); /* 稍微不透明一點以確保閱讀 */ border: 2px solid #EBB471; box-shadow: 0 7px 15px rgba(218, 165, 32, 0.15); border-top-left-radius: 4px; color: #5a4a42; /* 深咖啡色字體 */ } .ai-row .msg-content::before { display: block; font-size: 12px; color: #d4a017; font-weight: bold; margin-bottom: 6px; } /* --- User (右側) --- */ .user-row { justify-content: flex-end; } .user-row .msg-content { margin-right: 18px; margin-top: 10px; background: rgba(240, 217, 214, 0.75); border: 2px solid rgba(212, 66, 95, 0.5); box-shadow: 0 7px 15px rgba(255, 105, 180, 0.15); border-top-right-radius: 4px; color: #5a4a42; } .user-row .msg-content::before { display: none; /* 使用者通常不需要顯示自己的名字 */ } /* ========================================= 輸入框區域 (整合式膠囊設計) ========================================= */ /* 1. 容器變成大膠囊:負責背景、邊框、陰影 */ .input-controls { display: flex; align-items: center; width: 900px; /* 配合上方對話框寬度 */ max-width: 95vw; margin-bottom: 30px; flex-shrink: 0; /* 膠囊外觀核心 */ background: rgba(255, 255, 255, 0.6); /* 通透的半透明白 */ backdrop-filter: blur(10px); /* 毛玻璃效果 */ border: 1px solid rgba(255, 255, 255, 0.8); /* 極細的白邊框 */ border-radius: 50px; /* 大圓角,形成膠囊狀 */ padding: 6px 8px 6px 20px; /* 右側留少一點給按鈕,左側留多一點給文字 */ /* 陰影與層次 */ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.07); /* 動畫設定 (保留原本的浮現效果) */ animation: slideUp 0.8s ease-out 0.5s both; /* 互動過渡 */ transition: all 0.3s ease; box-sizing: border-box; z-index: 2; /* 確保浮在背景上 */ } /* 當使用者點擊輸入框時,讓整個容器發光 (Focus-within) */ .input-controls:focus-within { background: rgba(255, 255, 255, 0.85); box-shadow: 0 8px 32px rgba(255, 105, 180, 0.15); /* 淡淡的粉色光暈 */ transform: translateY(-2px); border-color: #fff; } /* 2. 輸入框變成透明內容:負責文字輸入 */ #message { flex-grow: 1; /* 佔滿剩餘空間 */ height: 40px; /* 高度適中 */ background: transparent; /* 關鍵:透明背景 */ border: none; /* 關鍵:移除邊框 */ outline: none; /* 移除點擊時的黑框 */ padding: 0; /* 內距交給容器處理 */ font-size: 16px; color: #4a4a4a; /* 深灰色文字,閱讀舒適 */ font-family: inherit; } /* Placeholder (提示文字) 樣式優化 */ #message::placeholder { color: rgba(100, 100, 100, 0.35); /* 極淺的灰色,更有質感 */ font-weight: 300; letter-spacing: 1px; } /* 3. 按鈕變成膠囊內的元件 */ #submit { flex-shrink: 0; width: 80px; /* 稍微縮小寬度 */ height: 40px; /* 高度配合輸入框 */ margin-left: 10px; /* 與文字保持距離 */ /* 漸層按鈕 */ background: linear-gradient(135deg, #DDACB2 0%, #C43739 100%); color: white; border: none; border-radius: 40px; /* 膠囊狀 */ cursor: pointer; font-weight: bold; font-size: 15px; letter-spacing: 1px; /* 按鈕陰影 */ box-shadow: 0 4px 15px rgba(255, 64, 129, 0.3); transition: all 0.3s ease; } /* 按鈕懸停效果 */ #submit:hover { transform: scale(1.05); /* 輕微放大 */ box-shadow: 0 6px 20px rgba(255, 64, 129, 0.5); background: linear-gradient(135deg, #DDACB2 0%, #C43739 100%); } #submit:active { transform: scale(0.95); } /* ========================================= 捲軸樣式設定 ========================================= */ /* 1. 設定捲軸整體的寬度 */ .scrollable-container::-webkit-scrollbar { width: 6px; } /* 2. 設定捲軸的「軌道」 (背景) */ .scrollable-container::-webkit-scrollbar-track { background: transparent; /* 改成透明 */ /* 讓捲軸上下內縮,不要頂天立地 */ margin-block: 20px; /* 上下各留 20px 的空白 */ border-radius: 10px; } /* 3. 設定捲軸的「滑塊」 (會動的那塊) */ .scrollable-container::-webkit-scrollbar-thumb { background-color: rgba(212, 66, 95, 0.5); border-radius: 10px; /* 增加一個懸停效果,滑鼠移過去變深,提示感更好 */ transition: background-color 0.3s; } /* [進階] 當滑鼠移到捲軸上時,顏色變深一點 */ .scrollable-container::-webkit-scrollbar-thumb:hover { background-color: rgba(212, 66, 95, 0.8); } ``` ::: --- ### js :::spoiler js ```javascript= $(function () { $("#submit").click(chatWithLLM); $("#message").keypress(function (e) { if (e.which == 13) { chatWithLLM(); } }); }); let message_count = 0; function chatWithLLM() { var $msgInput = $("#message"); var message = $msgInput.val(); // 防止傳送空白訊息 if (!message || message.trim().length === 0) { return; } $msgInput.val(""); message_count += 1; // 顯示使用者訊息 (User) // !!! 這裡改頭像圖片檔案 let userHtml = ` <div class="chat-row user-row"> <div class="msg-content user-content" id="msg-${message_count}"> ${escapeHtml(message)} </div> <img src="/static/images/girl.png" alt="User"> </div> `; $("#dialog-div").append(userHtml); scrollToBottom(); // 發送請求 var data = { message: message }; $.post("/call_llm", data, function (response) { message_count += 1; // 顯示 AI 訊息 (AI) // !!! 這裡改頭像圖片檔案 let aiHtml = ` <div class="chat-row ai-row"> <img src="/static/images/kamisama.png" alt="AI"> <div class="msg-content ai-content" id="msg-${message_count}"> ${response} </div> </div> `; $("#dialog-div").append(aiHtml); scrollToBottom(); }); } function scrollToBottom() { var $dialog = $("#dialog-div"); $dialog.scrollTop($dialog[0].scrollHeight); } function escapeHtml(text) { if (!text) return text; return text .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); } ``` ::: --- --- ## 1. Top_Score_Recommend Code :::info - 當我與對象配對分數小於40分時,自動啟動尋找另外三個高分配對 - **把對象排除**,其餘星座再一次Random配對分數,再排序取前三 - (25/12/07 update) 移除**排除自身**,邏輯上不需要排除自身 ::: :::spoiler Code :triangular_flag_on_post: ```python= import random import json from fastmcp import FastMCP import threading # 定義所有星座 (英文名/中文名) ALL_CONSTELLATIONS = [ "Aries/牡羊座", "Taurus/金牛座", "Gemini/雙子座", "Cancer/巨蟹座", "Leo/獅子座", "Virgo/處女座", "Libra/天秤座", "Scorpio/天蠍座", "Sagittarius/射手座", "Capricorn/摩羯座", "Aquarius/水瓶座", "Pisces/雙魚座" ] # 初始化 FastMCP 伺服器 mcp_nav = FastMCP(name="top_score_server") @mcp_nav.tool def get_top_matches(my_sign: str, partner_sign: str) -> str: """ 計算提問者與其他星座的隨機高分速配指數,並回傳 Top 3 名單。 必須排除原始查詢的對象 (partner_sign)。 Args: my_sign (str): 提問者自己的星座名稱 (例如:雙魚座)。 partner_sign (str): 原始查詢的伴侶星座名稱 (例如:天蠍座,必須被排除)。 Returns: str: 包含 Top 3 星座和分數的 JSON 格式字串。 """ results = [] # 標準化輸入 (用於比較和排除) my_sign_standard = my_sign.lower().split('/')[0] partner_sign_standard = partner_sign.lower().split('/')[0] # 獲取需要排除的對象 # 遍歷所有星座進行計算 for full_other_sign in ALL_CONSTELLATIONS: parts = full_other_sign.split('/') other_sign_en = parts[0].lower() other_sign_cn = parts[1] # 排除原始查詢的對象 (例如:如果查詢天蠍,就不能推薦天蠍) if other_sign_en == partner_sign_standard: continue # 模擬計算分數 (簡單隨機高分區間) score = random.randint(80, 100) results.append({ "constellation": other_sign_cn, "score": score }) # 根據分數降序排序 top_3 = sorted(results, key=lambda x: x['score'], reverse=True)[:3] # 將 Top 3 列表轉換為 JSON 字串回傳給 Agent # 這裡不需要將 threading 導入,因為 FastMCP 內部會處理 return json.dumps(top_3, ensure_ascii=False) if __name__ == "__main__": print("get_top_matches Server is starting...") # 使用 SSE 傳輸模式,運行在 Port 5006 mcp_nav.run(transport="sse", port=5006) ``` ::: ## 2. Agent Code :::info 主要更改instructor,加入 Top_Score_Recommend mcp,效果如圖 ::: :::spoiler Code ```python= import datetime from zoneinfo import ZoneInfo from google.adk.agents import LlmAgent from google.adk.tools.mcp_tool.mcp_toolset import ( MCPToolset, StdioServerParameters, SseConnectionParams, ) import requests import os from dotenv import load_dotenv, dotenv_values # Load environment variables from .env file load_dotenv() # --- 內部 Python 函式 (保留不變) --- # 為了程式碼的完整性,保留 get_weather 和 get_current_time 函式定義 def get_weather(city: str) -> dict: """Retrieves the current weather report for a specified city.""" api_key = os.getenv("OPEN_WEATHER_MAP_API_KEY") if not api_key: return {"status": "error", "error_message": "API key for OpenWeatherMap is not set."} url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric" try: response = requests.get(url) response.raise_for_status() data = response.json() if data["cod"] != 200: return {"status": "error", "error_message": f"Weather information for '{city}' is not available."} weather_description = data["weather"][0]["description"] temperature = data["main"]["temp"] report = ( f"The weather in {city} is {weather_description} with a temperature of " f"{temperature} degrees Celsius." ) return {"status": "success", "report": report} except requests.exceptions.RequestException as e: return {"status": "error", "error_message": f"An error occurred while fetching the weather data: {str(e)}"} def get_current_time(tz_identifier: str) -> dict: """Returns the current time in a specified time zone identifier.""" try: tz = ZoneInfo(tz_identifier) now = datetime.datetime.now(tz) report = f'The current time is {now.strftime("%Y-%m-%d %H:%M:%S %Z%z")}' return {"status": "success", "report": report} except Exception as e: return {"status": "error", "error_message": f"An error occurred while fetching the current time: {str(e)}"} # --- 代理定義與工具整合 (修正版) --- root_agent = LlmAgent( name="LoveMatching_agent", model="gemini-2.5-flash", description=( "Agent is a 'Matchmaking Cupid' (月老) AI specializing in relationship guidance, " "horoscope matching, and providing advice to users confused or deeply involved " "in love. It can also provide weather information and access files." ), instruction=( """ 你是掌管緣分的「月老」AI,專門為對愛情感到迷茫或暈船的使用者提供指引和建議。 請用繁體中文,並使用月老或長輩的語氣來回答問題。 【月老配對任務流程】 當使用者詢問星座速配時,你**必須**執行以下步驟: 1. 必須呼叫 `match_index_server:get_match_index(sign1, sign2)` 獲取速配指數 (Score)。 2. IF Score < 40 (低分): 這是【複合導航任務】。你必須嚴格按照以下順序執行: a. 呼叫 `top_score_server:get_top_matches(sign1, sign2)` 獲取三個高分推薦名單。 b. 接著,呼叫 `low_advice_server:get_low_advice` 獲取建議。 3. IF Score >= 40 (高分): 必須呼叫 `high_advice_server:get_high_advice` 獲取建議。 【通用指引】 * 在呼叫工具 (例如 get_weather) 時,city name 必須使用英文。 * 當使用者的需求跟檔案存取有關時,請先呼叫 list_allowed_directories 取得預設檔案存取的位置。 """ ), tools=[ get_weather, get_current_time, # 1. 核心分數計算 MCP (Port 5004) - 獲取單點分數 MCPToolset( connection_params=SseConnectionParams(url="http://127.0.0.1:5004/sse"), ), # 2. 導航名單 MCP (Port 5006) - 獲取 Top 3 名單 (與先前配置一致) MCPToolset( connection_params=SseConnectionParams(url="http://127.0.0.1:5006/sse"), ), # 3. 高分建議 MCP (新增/修正 Port 5007) MCPToolset( connection_params=SseConnectionParams(url="http://127.0.0.1:5005/sse"), ), # 5. 檔案存取 MCP (保留不變) MCPToolset( connection_params=StdioServerParameters( command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/Users/suyuhan/SynologyDrive/03_Coding/test_file_20250922"], ), ), # 6. 自定義工具範例 (保留不變) MCPToolset( connection_params=StdioServerParameters( command="/Users/suyuhan/.local/bin/uv", args=["--directory", "/Users/suyuhan/SynologyDrive/03_Coding/114_1_Weather2Mood", "run", "server.py"], ), ), ], ) ``` ::: ![Screenshot 2025-11-29 at 17.35.08](https://hackmd.io/_uploads/BJi804uZZg.png)