第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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…