第2篇:數據持久化實戰

在上一篇中,我們構建了一個基于內存存儲的食譜助手。說實話,內存存儲雖然簡單,但有個致命問題:程序一重啟,數據就全沒了。

所以這篇我們要解決數據持久化的問題,將食譜助手從內存存儲升級到SQLite數據庫。

項目結構重組

先把項目結構整理得更清晰一些。之前我們所有代碼都在一個文件里,現在按功能分模塊:

mkdir -p recipe-assistant/app recipe-assistant/data
cd recipe-assistant
touch app/__init__.py app/main.py app/models.py

新的目錄結構:

recipe-assistant/app/__init__.pymain.py          # 主應用入口(包含所有MCP組件)models.py        # 數據模型和數據庫管理data/recipes.db       # SQLite數據庫文件bb

重要說明:FastMCP不支持模塊化的方式(如include_router),所有的資源、工具和提示詞組件都必須定義在同一個FastMCP實例上。因此我們將所有MCP組件都放在main.py中。

數據庫設計

創建 app/models.py,定義數據模型和數據庫操作:

# app/models.py
from pydantic import BaseModel
from typing import List, Optional, Dict
import sqlite3
import json
import os# 數據模型定義(與第1篇保持一致)
class Ingredient(BaseModel):name: strquantity: strclass Recipe(BaseModel):id: strname: strcuisine: strdescription: stringredients: List[Ingredient]steps: List[str]difficulty: strclass UserPreference(BaseModel):user_id: strfavorite_cuisines: List[str]dietary_restrictions: List[str]cooking_skill: str# 數據庫管理類
class DatabaseManager:def __init__(self, db_path="data/recipes.db"):self.db_path = db_pathself.conn = Noneself.initialize_db()def get_connection(self):if self.conn is None:# 確保數據目錄存在os.makedirs(os.path.dirname(self.db_path), exist_ok=True)self.conn = sqlite3.connect(self.db_path)self.conn.execute("PRAGMA foreign_keys = ON")self.conn.row_factory = sqlite3.Rowreturn self.conndef initialize_db(self):"""創建數據庫表并導入第1篇的示例數據"""conn = self.get_connection()cursor = conn.cursor()# 創建食譜表cursor.execute('''CREATE TABLE IF NOT EXISTS recipes (id TEXT PRIMARY KEY,name TEXT NOT NULL,cuisine TEXT NOT NULL,description TEXT,ingredients TEXT NOT NULL,steps TEXT NOT NULL,difficulty TEXT NOT NULL)''')# 創建用戶偏好表cursor.execute('''CREATE TABLE IF NOT EXISTS user_preferences (user_id TEXT PRIMARY KEY,favorite_cuisines TEXT NOT NULL,dietary_restrictions TEXT NOT NULL,cooking_skill TEXT NOT NULL)''')conn.commit()# 導入第1篇的示例數據self.import_data()def import_data(self):"""導入第1篇的示例數據"""conn = self.get_connection()cursor = conn.cursor()# 檢查是否已有數據cursor.execute("SELECT COUNT(*) FROM recipes")count = cursor.fetchone()[0]if count == 0:# 第1篇的示例食譜數據recipes = [{"id": "recipe_001","name": "宮保雞丁","cuisine": "川菜","description": "經典川菜,麻辣鮮香","ingredients": [{"name": "雞胸肉", "quantity": "300g"},{"name": "花生米", "quantity": "50g"},{"name": "干辣椒", "quantity": "10個"},{"name": "花椒", "quantity": "1茶匙"},{"name": "蔥", "quantity": "2根"},{"name": "姜", "quantity": "3片"},{"name": "蒜", "quantity": "3瓣"}],"steps": ["雞胸肉切丁,用料酒、生抽、淀粉腌制15分鐘","熱鍋涼油,放入花椒和干辣椒爆香","加入雞丁翻炒至變色","加入蔥姜蒜繼續翻炒","加入調好的宮保汁炒勻","最后加入花生米炒勻即可"],"difficulty": "中等"},{"id": "recipe_002","name": "番茄炒蛋","cuisine": "家常菜","description": "簡單易做的家常菜","ingredients": [{"name": "番茄", "quantity": "2個"},{"name": "雞蛋", "quantity": "3個"},{"name": "蔥", "quantity": "適量"},{"name": "鹽", "quantity": "適量"},{"name": "糖", "quantity": "少許"}],"steps": ["番茄切塊,雞蛋打散","熱鍋倒油,倒入雞蛋炒熟盛出","鍋中再倒少許油,放入番茄翻炒","番茄出汁后加入鹽和糖調味","倒入炒好的雞蛋翻炒均勻","撒上蔥花即可"],"difficulty": "簡單"}]# 插入食譜數據for recipe in recipes:cursor.execute('''INSERT INTO recipes (id, name, cuisine, description, ingredients, steps, difficulty)VALUES (?, ?, ?, ?, ?, ?, ?)''', (recipe["id"],recipe["name"],recipe["cuisine"],recipe["description"],json.dumps(recipe["ingredients"], ensure_ascii=False),json.dumps(recipe["steps"], ensure_ascii=False),recipe["difficulty"]))# 插入示例用戶偏好preferences = {"user_001": {"favorite_cuisines": ["川菜"],"dietary_restrictions": ["少油", "少鹽"],"cooking_skill": "初級"},"user_002": {"favorite_cuisines": ["家常菜"],"dietary_restrictions": ["健康"],"cooking_skill": "初級"}}for user_id, prefs in preferences.items():cursor.execute('''INSERT INTO user_preferences (user_id, favorite_cuisines, dietary_restrictions, cooking_skill)VALUES (?, ?, ?, ?)''', (user_id,json.dumps(prefs["favorite_cuisines"], ensure_ascii=False),json.dumps(prefs["dietary_restrictions"], ensure_ascii=False),prefs["cooking_skill"]))conn.commit()print("示例數據導入完成")# 食譜相關操作def get_all_recipes(self):"""獲取所有食譜"""conn = self.get_connection()cursor = conn.cursor()cursor.execute("SELECT * FROM recipes")rows = cursor.fetchall()recipes = []for row in rows:recipe = {"id": row["id"],"name": row["name"],"cuisine": row["cuisine"],"description": row["description"],"ingredients": json.loads(row["ingredients"]),"steps": json.loads(row["steps"]),"difficulty": row["difficulty"]}recipes.append(recipe)return recipesdef get_recipe_by_id(self, recipe_id: str):"""根據ID獲取食譜"""conn = self.get_connection()cursor = conn.cursor()cursor.execute("SELECT * FROM recipes WHERE id = ?", (recipe_id,))row = cursor.fetchone()if row:return {"id": row["id"],"name": row["name"],"cuisine": row["cuisine"],"description": row["description"],"ingredients": json.loads(row["ingredients"]),"steps": json.loads(row["steps"]),"difficulty": row["difficulty"]}return Nonedef search_recipes_by_ingredient(self, ingredient: str):"""根據食材搜索食譜"""conn = self.get_connection()cursor = conn.cursor()cursor.execute("SELECT * FROM recipes WHERE ingredients LIKE ?", (f'%{ingredient}%',))rows = cursor.fetchall()recipes = []for row in rows:recipe = {"id": row["id"],"name": row["name"],"cuisine": row["cuisine"],"description": row["description"],"ingredients": json.loads(row["ingredients"]),"steps": json.loads(row["steps"]),"difficulty": row["difficulty"]}recipes.append(recipe)return recipes# 用戶偏好相關操作def get_user_preferences(self, user_id: str):"""獲取用戶偏好"""conn = self.get_connection()cursor = conn.cursor()cursor.execute("SELECT * FROM user_preferences WHERE user_id = ?", (user_id,))row = cursor.fetchone()if row:return {"user_id": row["user_id"],"favorite_cuisines": json.loads(row["favorite_cuisines"]),"dietary_restrictions": json.loads(row["dietary_restrictions"]),"cooking_skill": row["cooking_skill"]}return None# 全局數據庫實例
db = DatabaseManager()def _get_all_recipes() -> Dict:"""獲取所有食譜數據"""try:recipes = db.get_all_recipes()return {"success": True,"recipes": recipes,"count": len(recipes)}except Exception as e:return {"success": False,"error": f"獲取食譜數據失敗: {str(e)}"}def _get_recipe_by_id(recipe_id: str) -> Dict:"""根據ID獲取特定食譜"""try:recipe = db.get_recipe_by_id(recipe_id)if recipe:return {"success": True,"recipe": recipe}else:return {"success": False,"error": f"未找到ID為{recipe_id}的食譜"}except Exception as e:return {"success": False,"error": f"獲取食譜失敗: {str(e)}"}def _get_user_preferences(user_id: str) -> Dict:"""獲取用戶偏好數據"""try:preferences = db.get_user_preferences(user_id)if preferences:return {"success": True,"preferences": preferences}else:return {"success": False,"error": f"未找到ID為{user_id}的用戶"}except Exception as e:return {"success": False,"error": f"獲取用戶偏好失敗: {str(e)}"}def _search_recipes_by_ingredient(ingredient: str) -> Dict:"""根據食材查詢食譜"""try:if not ingredient or not ingredient.strip():return {"success": False,"error": "請提供有效的食材名稱"}recipes = db.search_recipes_by_ingredient(ingredient.strip())if recipes:return {"success": True,"message": f"找到了{len(recipes)}個包含{ingredient}的食譜","recipes": recipes}else:return {"success": True,"message": f"抱歉,沒有找到包含{ingredient}的食譜","recipes": []}except Exception as e:return {"success": False,"error": f"查詢食譜時出錯: {str(e)}"}def _recommend_recipes(user_id: str, available_ingredients: List[str] = None) -> Dict:"""根據用戶偏好推薦食譜"""try:# 獲取用戶偏好user_prefs = db.get_user_preferences(user_id)if not user_prefs:return {"success": False,"error": f"未找到ID為{user_id}的用戶偏好"}# 獲取所有食譜all_recipes = db.get_all_recipes()recommended_recipes = []for recipe in all_recipes:# 根據用戶喜好的菜系過濾if recipe["cuisine"] in user_prefs["favorite_cuisines"]:# 如果提供了可用食材,檢查是否匹配if available_ingredients:recipe_ingredients = [ing["name"] for ing in recipe["ingredients"]]if any(avail_ing in recipe_ingredients for avail_ing in available_ingredients):recommended_recipes.append(recipe)else:recommended_recipes.append(recipe)return {"success": True,"message": f"為您推薦了{len(recommended_recipes)}道菜","recipes": recommended_recipes[:5]  # 限制返回數量}except Exception as e:return {"success": False,"error": f"推薦食譜時出錯: {str(e)}"}

這個數據庫管理類把之前內存中的操作都移到了SQLite里。

主應用入口

現在創建 app/main.py,將所有MCP組件集中在一個文件中:

# app/main.py
from fastmcp import FastMCP
from typing import Dict, List
from .models import (_get_all_recipes, _get_recipe_by_id, _get_user_preferences,_search_recipes_by_ingredient,_recommend_recipes
)# 創建主應用實例
mcp = FastMCP("RecipeAssistant")# 資源組件
@mcp.resource("recipes://all")
def get_all_recipes() -> Dict:"""獲取所有食譜數據Returns:包含所有食譜的字典"""return _get_all_recipes()@mcp.resource("recipes://{recipe_id}")
def get_recipe_by_id(recipe_id: str) -> Dict:"""根據ID獲取特定食譜Args:recipe_id: 食譜IDReturns:食譜詳細信息"""return _get_recipe_by_id(recipe_id)@mcp.resource("users://{user_id}/preferences")
def get_user_preferences(user_id: str) -> Dict:"""獲取用戶偏好數據Args:user_id: 用戶IDReturns:用戶偏好數據"""return _get_user_preferences(user_id)# 工具組件
@mcp.tool()
def search_recipes_by_ingredient(ingredient: str) -> Dict:"""根據食材查詢食譜Args:ingredient: 食材名稱Returns:包含匹配食譜的字典"""return _search_recipes_by_ingredient(ingredient)@mcp.tool()
def recommend_recipes(user_id: str, available_ingredients: List[str] = None) -> Dict:"""根據用戶偏好和可用食材推薦食譜Args:user_id: 用戶IDavailable_ingredients: 可用食材列表(可選)Returns:包含推薦食譜的字典"""return _recommend_recipes(user_id, available_ingredients)# 提示詞組件
@mcp.prompt()
def generate_recipe_search_response(ingredient: str) -> str:"""生成食譜查詢的回復Args:ingredient: 食材名稱Returns:格式化的回復文本"""search_result = _search_recipes_by_ingredient(ingredient)if not search_result["success"]:return f"抱歉,查詢食譜時出現了問題:{search_result.get('error', '未知錯誤')}"recipes = search_result["recipes"]if not recipes:return f"抱歉,我沒有找到包含{ingredient}的食譜。請嘗試其他食材。"# 生成回復文本response = f"我找到了{len(recipes)}個包含{ingredient}的食譜:\n\n"for i, recipe in enumerate(recipes, 1):response += f"{i}. {recipe['name']} - {recipe['description']}\n"response += f"   難度:{recipe['difficulty']}\n"response += f"   主要食材:{', '.join(ing['name'] for ing in recipe['ingredients'][:3])}\n\n"response += f"想了解某個食譜的詳細做法,請告訴我食譜的編號。"return response@mcp.prompt()
def generate_recipe_details(recipe_id: str) -> str:"""生成食譜詳細信息的回復Args:recipe_id: 食譜IDReturns:格式化的食譜詳情"""recipe_result = _get_recipe_by_id(recipe_id)if not recipe_result["success"]:return f"抱歉,無法獲取食譜詳情:{recipe_result.get('error', '未知錯誤')}"recipe = recipe_result["recipe"]# 生成詳細食譜信息response = f"# {recipe['name']}\n\n"response += f"{recipe['description']}\n\n"response += "## 食材準備\n\n"for ing in recipe["ingredients"]:response += f"- {ing['name']}: {ing['quantity']}\n"response += "\n## 烹飪步驟\n\n"for i, step in enumerate(recipe["steps"], 1):response += f"{i}. {step}\n"response += f"\n難度:{recipe['difficulty']}\n"response += f"菜系:{recipe['cuisine']}\n"return responseif __name__ == "__main__":mcp.run()

測試驗證

現在來測試我們升級后的應用。

使用MCP Inspector測試

首先,啟動我們的應用:

cd recipe-assistant
python -m app.main

在另一個終端啟動MCP Inspector測試,可以按照我們之前的方式進行測試:

npx @modelcontextprotocol/inspector stdio python -m app.main

下一篇預告:我們將通過實際代碼對比,學習如何將一個FastMCP工具改寫成原生SDK版本,直觀理解兩者的差異和使用場景。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/98978.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/98978.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/98978.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Java推薦系統與機器學習實戰案例

基于Java的推薦系統與機器學習實例 以下是一些基于Java的推薦系統與機器學習實例的參考方向及開源項目,涵蓋協同過濾、矩陣分解、深度學習等常見方法。內容根據實際項目和技術文檔整理,可直接用于學習或開發。 協同過濾實現 用戶-物品評分預測 使用Apache Mahout的基于用戶…

AI生成內容檢測的綜合方法論與技術路徑

一、AI內容檢測技術的分類與原理當前AI內容檢測技術主要分為四大類,每類都有其獨特的原理和應用場景:1. 基于語言特征分析的檢測方法這類方法通過挖掘人類寫作與AI生成文本之間的統計學差異進行判斷:1.1 詞匯使用模式分析AI生成的文本在詞匯選…

可可圖片編輯 HarmonyOS(5)濾鏡效果

可可圖片編輯 HarmonyOS(5)濾鏡效果 前言 可可圖片編輯也實現了濾鏡效果,主要是利用 Image組件的 colorFilter 屬性實現。濾鏡的關鍵屬性 colorFilter colorFilter 的主要作用是給圖像設置顏色濾鏡效果。 其核心原理是使用一個 4x5 的顏色矩陣…

< JS事件循環系列【二】> 微任務深度解析:從本質到實戰避坑

在上一篇關于 JS 事件循環的文章中,我們提到 “微任務優先級高于宏任務” 這一核心結論,但對于微任務本身的細節并未展開。作為事件循環中 “優先級最高的異步任務”,微任務的執行機制直接影響代碼邏輯的正確性,比如Promise.then的…

STM32 單片機開發 - SPI 總線

一、SPI 總線概念SPI 總線 --- Serial Peripheral Interface,即串行外設接口SPI 是摩托羅拉公司設計的一款 串行、同步、全雙工總線;SPI 總線是三線 / 四線制總線,分別是:SPI_SCK(時鐘線)、S…

區域醫院云HIS系統源碼,云醫院管理系統源碼,云診所源碼

云HIS源碼,云醫院管理系統源碼,云診所源碼,二級專科醫院云HIS系統源代碼,有演示云HIS,即云醫院管理系統,是一種運用云計算、大數據、物聯網等新興信息技術的醫療信息化解決方案。它重新定義了傳統的醫院信息…

Java基礎 9.11

1.第三代日期類前面兩代日期類的不足分析JDK 1.0中包含了一個java.uti.Date類,但是它的大多數方法已經在JDK1.1引Calendar類之后被棄用了。而Calendar也存在問題是:可變性:像日期和時間這樣的類應該是不可變的偏移性:Date中的年份…

JavaScript 數組過濾方法

在 JavaScript 編程中,數組是最常用的數據結構之一,而數組過濾則是處理數據集合的關鍵操作。filter() 方法提供了一種高效的方式來從數組中篩選出符合特定條件的元素,返回一個新的數組,而不改變原始數組。這種方法在處理對象數組時…

《sklearn機器學習——數據預處理》離散化

sklearn 數據預處理中的離散化(Discretization) 離散化是將連續型數值特征轉換為離散區間(分箱/bins)的過程,常用于簡化模型、增強魯棒性、處理非線性關系或滿足某些算法對離散輸入的要求(如樸素貝葉斯、決…

PTA算法簡析

ArkAnalyzer源碼初步分析I:https://blog.csdn.net/2302_80118884/article/details/151627341?spm1001.2014.3001.5501 首先,我們必須明確 PTA 的核心工作:它不再關心變量的“聲明類型”,而是為程序中的每一個變量和每一個對象字段…

Vue 3 中監聽多個數據變化的幾種方法

1. 使用 watch監聽多個 ref/reactive 數據import { ref, watch } from vueexport default {setup() {const count ref(0)const name ref()const user reactive({ age: 20 })// 監聽多個數據源watch([count, name, () > user.age], // 數組形式傳入多個數據源([newCount, …

第 2 篇:Java 入門實戰(JDK8 版)—— 編寫第一個 Java 程序,理解基礎運行邏輯

用 IntelliJ IDEA 寫第一個 Java 8 程序:Hello World 實操指南 作為 Java 初學者,“Hello World” 是你接觸這門語言的第一個里程碑。本文會聚焦 Java 8(經典 LTS 版本,企業級開發常用) 和 IntelliJ IDEA(當…

【GPT入門】第67課 多模態模型實踐: 本地部署文生視頻模型和圖片推理模型

【GPT入門】第67課 多模態模型實踐: 本地部署文生視頻模型和圖片推理模型1. 文生視頻模型CogVideoX-5b 本地部署1.1 模型介紹1.2 環境安裝1.3 模型下載1.4 測試2.ollama部署圖片推理模型 llama3.2-vision2.1 模型介紹2.2 安裝ollama2.3 下載模型2.4 測試模型2.5 測試…

C++初階(6)類和對象(下)

1. 再談構造函數(構造函數的2個深入使用技巧) 1.1 構造函數體賦值 在創建對象時,編譯器通過調用構造函數,給對象中各個成員變量一個合適的初始值。 雖然上述構造函數調用之后,對象中已經有了一個初始值,…

容器文件描述符熱遷移在云服務器高可用架構的實施標準

在云計算環境中,容器文件描述符熱遷移技術正成為保障業務連續性的關鍵解決方案。本文將深入解析該技術在云服務器高可用架構中的實施標準,涵蓋技術原理、實現路徑、性能優化等核心維度,為構建穩定可靠的容器化基礎設施提供系統化指導。 容器文…

毫米波雷達液位計如何遠程監控水位?

引言毫米波雷達液位計作為一種高精度、非接觸式的水位監測設備,正逐漸成為智慧水務、環境監測等領域的關鍵工具。其通過先進的調頻連續波(FMCW)技術,實現5mm的測量精度,并支持多種遠程通信方式,使用戶能夠實…

關于 C++ 編程語言常見問題及技術要點的說明

關于 C 編程語言常見問題及技術要點的說明C 作為一門兼具高效性與靈活性的靜態編譯型編程語言,自 1985 年正式發布以來,始終在系統開發、游戲引擎、嵌入式設備、高性能計算等領域占據核心地位。隨著 C 標準(如 C11、C17、C20)的持…

【Qt QSS樣式設置】

Qt中的QSS樣式設置流程 Qt Style Sheets (QSS) 是Qt框架中用于自定義控件外觀的樣式表語言,其語法類似于CSS。以下是QSS的設置流程和示例。 QSS設置流程 1. 創建QSS樣式表文件或字符串 首先,需要創建QSS樣式表,可以是一個單獨的.qss文件&…

使用 Apollo TransformWrapper 生成相機到各坐標系的變換矩陣

使用 Apollo TransformWrapper 生成相機到各坐標系的變換矩陣一、背景二、原理1、什么是變換矩陣?2、為什么需要變換矩陣?3、Apollo 中的坐標系4、Apollo TransformWrapper三、操作步驟1. 設置車輛參數2. 啟動靜態變換發布3. 查看變換信息4. 播放記錄數據…

硬件(十)IMX6ULL 中斷與時鐘配置

一、OCP 原則(開閉原則)對代碼擴展是開放的,允許通過新增代碼來擴展功能;對代碼的修改是關閉的,盡量避免直接修改已有穩定運行的代碼,以此保障代碼的穩定性與可維護性。二、中斷處理(一&#xf…