BrianChuan
Update RAG and weather search
bc019c8
Raw
History Blame Contribute Delete
9.42 kB
import gradio as gr
import google.generativeai as genai
import os
from dotenv import load_dotenv
import json
import pandas as pd
import re
import tempfile
import textwrap
from ddgs import DDGS
load_dotenv() # 初始化:載入 .env 檔案中的變數
# --- 1. 設定 Google Gemini API ---
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
print("❌ 警告:未偵測到 API Key,請在 Settings > Secrets 中設定 'GEMINI_API_KEY'。")
else:
genai.configure(api_key=api_key)
# --- 外包資訊獲取函式 (使用 DuckDuckGo) ---
def search_weather_info(location, date):
"""
使用 DuckDuckGo 搜尋當地的天氣資訊,不需 API Key。
"""
search_query = f"{location} {date} 天氣 氣溫 降雨機率 中央氣象署"
print(f"🔍 正在搜尋:{search_query}")
try:
results = DDGS().text(search_query, max_results=3)
# 將搜尋結果組合成一段文字
context_text = ""
for res in results:
context_text += f"- 標題:{res['title']}\n 內容:{res['body']}\n"
return context_text
except Exception as e:
print(f"⚠️ 搜尋失敗: {e}")
return "無法取得即時天氣資訊,請依據歷史氣候推估。"
# --- 2. RAG 知識庫載入與檢索邏輯 ---
RAG_FILE = "rag_knowledge.json"
KNOWLEDGE_BASE = []
def load_knowledge_base():
"""程式啟動時載入 JSON 資料集"""
global KNOWLEDGE_BASE
if os.path.exists(RAG_FILE):
try:
with open(RAG_FILE, "r", encoding="utf-8") as f:
KNOWLEDGE_BASE = json.load(f)
print(f"📚 RAG 知識庫載入成功!共 {len(KNOWLEDGE_BASE)} 筆片段。")
except Exception as e:
print(f"⚠️ 知識庫讀取失敗: {e}")
else:
print(f"⚠️ 找不到 {RAG_FILE},將無法使用 RAG 功能。")
# 執行載入
load_knowledge_base()
def retrieve_context(activity):
"""
根據活動類型,從知識庫中篩選相關的內容
這是一個輕量化的 Metadata Filtering RAG
"""
if not KNOWLEDGE_BASE:
return "無知識庫資料"
related_chunks = []
# 定義關鍵字對映 (Activity -> PDF Filename keywords)
keywords = []
if activity == "登山健行":
keywords = ["登山", "玉山"] # 會抓取「登山裝備」和「玉山檢查表」
elif activity == "自行車騎行":
keywords = ["單車"]
elif activity == "馬拉松":
keywords = ["跑步"]
# 篩選相關片段
for chunk in KNOWLEDGE_BASE:
source_name = chunk.get("source", "")
# 如果來源檔名包含對應的關鍵字,就納入 Context
if any(k in source_name for k in keywords):
# 格式化內容:[來源: 頁數] 內容...
formatted_text = f"[出處: {os.path.basename(source_name)} P.{chunk['page']}]\n{chunk['content']}"
related_chunks.append(formatted_text)
# 將所有片段合併成一個字串
if related_chunks:
print(f"🔍 RAG 檢索: 針對 '{activity}' 找到了 {len(related_chunks)} 個相關片段。")
return "\n\n".join(related_chunks)
else:
print(f"⚠️ RAG 檢索: 找不到 '{activity}' 的相關資料。")
return "無相關知識庫資料"
# --- 3. 核心邏輯 ---
def adapt_gear_advisor(activity, location, date, duration, feedback):
print(f"--- 開始處理請求: {activity} @ {location} ---")
# 先執行「外包」的搜尋工作
weather_context = search_weather_info(location, date)
print(f"📄 獲取外部資訊長度: {len(weather_context)} 字")
# 步驟 A: 執行 RAG 檢索
rag_context = retrieve_context(activity)
# 步驟 B: 設定模型 (啟用 Google Search)
model_name = 'models/gemini-2.5-flash'
try:
model = genai.GenerativeModel(model_name)
except Exception as e:
return f"❌ 模型初始化失敗: {str(e)}", None, None
# 步驟 C: 組合 Prompt (注入 RAG Context)
prompt = f"""
你是一個專業的戶外運動裝備顧問 'AdaptGear'。
【外部搜尋到的天氣資訊】
以下是剛剛從網路上搜尋到的真實資料:
{weather_context}
【任務目標】
1. **天氣推估**:請根據 '{location}' 與 '{date}',自行推估該季節的平均氣候(氣溫、降雨機率)。
2. **核心任務**:參考下方的【RAG 知識庫內容】,生成一份符合該活動規範的客製化裝備清單。
請優先使用知識庫中提到的專業裝備名稱(例如:若知識庫提到「GTX 外套」,就不要只寫「雨衣」)。
3. 結合使用者的【歷史回饋】進行反思與調整。
【RAG 知識庫內容 (請嚴格參考此資料建立清單)】
{rag_context}
【使用者輸入】
- 活動類型:{activity}
- 地點/路線:{location}
- 日期與時間:{date}
- 行程時長:{duration}
- 歷史回饋:{feedback if feedback else "無"}
【輸出格式要求】
只輸出一個標準 JSON 物件:
{{
"weather_forecast": "真實天氣數據 (氣溫/降雨機率)",
"smart_advice": "結合「RAG 知識庫規範」與「使用者回饋」的綜合建議",
"checklist": [
{{ "category": "分類(如:個人裝備)", "item": "裝備名稱", "reason": "說明(例如: 依據玉山檢查表規定必帶)", "quantity": "數量" }},
...
]
}}
"""
try:
print(f"正在呼叫模型 (含搜尋 & RAG)...")
response = model.generate_content(prompt)
print("✅ 生成成功")
raw_text = response.text
# 解析 JSON
try:
# 1. 尋找字串中第一個 '{' 和最後一個 '}'
start_idx = raw_text.find('{')
end_idx = raw_text.rfind('}') + 1
if start_idx != -1 and end_idx != -1:
# 只擷取大括號中間的內容 (過濾掉前面的 "Here is JSON" 或後面的雜訊)
json_str = raw_text[start_idx:end_idx]
data = json.loads(json_str)
else:
raise ValueError("無法在回應中找到 JSON 區塊 (找不到大括號)")
except json.JSONDecodeError as e:
# 如果還是失敗,印出原始文字以供除錯
print(f"❌ JSON 解析失敗。原始回應:\n{raw_text}")
return f"解析錯誤:模型回傳了非 JSON 格式的內容。\n錯誤細節: {e}", None, None
# 處理顯示文字
weather_info = data.get('weather_forecast', '無資料')
advice_info = data.get('smart_advice', '無資料')
display_text = (
f"## 🌤️ 天氣預測\n\n"
f"{weather_info}\n\n"
f"## 💡 AdaptGear 叮嚀\n\n"
f"{advice_info}\n\n"
)
# 處理表格與下載
checklist = data.get('checklist', [])
df = pd.DataFrame(checklist)
df.columns = ['類別', '裝備項目', '推薦理由', '數量']
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode='w', encoding='utf-8-sig') as tmp:
df.to_csv(tmp.name, index=False)
file_path = tmp.name
return display_text, df, file_path
except Exception as e:
error_msg = str(e)
print(f"❌ 生成失敗: {error_msg}")
return f"發生錯誤:{error_msg}", None, None
# --- 3. 建置 Gradio 介面 ---
with gr.Blocks() as demo:
gr.Markdown("# 🏔️ AdaptGear 運動裝備智慧顧問")
with gr.Row():
with gr.Column(scale=1):
input_activity = gr.Dropdown(
label="活動類型",
choices=[
"登山健行",
"自行車騎行",
"馬拉松",
],
value="登山健行"
)
input_location = gr.Textbox(label="地點/路線", value="嘉明湖", placeholder="例如:嘉明湖")
input_date = gr.DateTime(label="出發日期", include_time=False, type="string")
input_duration = gr.Textbox(label="行程時長", value="三天兩夜", placeholder="例如:三天兩夜")
input_feedback = gr.Textbox(label="歷史回饋 (選填)", lines=3, placeholder="例如:上次覺得水帶不夠...")
submit_btn = gr.Button("生成裝備清單", variant="primary")
with gr.Column(scale=2):
# 區塊 1: 智慧建議與天氣 (文字)
output_advice = gr.Markdown(label="智慧分析")
# 區塊 2: 裝備清單 (表格) - 看起來更有系統性
output_table = gr.Dataframe(
headers=["類別", "裝備項目", "推薦理由", "數量"],
label="客製化裝備清單",
interactive=False
)
# 區塊 3: 下載按鈕
output_file = gr.File(label="下載清單 (.csv)")
submit_btn.click(
fn=adapt_gear_advisor,
inputs=[input_activity, input_location, input_date, input_duration, input_feedback],
outputs=[output_advice, output_table, output_file]
)
# --- 4. 啟動應用 ---
if __name__ == "__main__":
demo.launch()