---
# System prepended metadata

title: AI人工智慧導論期末專題

---

# 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)
