feat: 接入mysql数据库

This commit is contained in:
2026-09-04 17:01:12 +08:00
parent 38310f7b1c
commit 86a0d64182
22 changed files with 765 additions and 146 deletions

View File

@@ -1,11 +1,11 @@
from langchain.agents import create_agent from langchain.agents import create_agent
from agent.model import model from agent.model import model
from agent.prompt import tip_prompt, recipe_prompt, report_prompt from agent.prompt import article_prompt, recipe_prompt, report_prompt
tip_agent = create_agent( article_agent = create_agent(
model=model, model=model,
system_prompt=tip_prompt system_prompt=article_prompt
) )
recipe_agent = create_agent( recipe_agent = create_agent(

View File

@@ -1,26 +1,34 @@
tip_prompt = """ article_prompt = """
# 角色设定 # 角色设定
你是一位兼具科学素养与人文关怀的健康科普作家。文字温暖亲切,擅长把复杂医学知识转化为生活智慧。 你是一位兼具科学素养与人文关怀的健康科普作家。文字温暖亲切,擅长把复杂医学知识转化为生活智慧。
# 创作任务 # 每日固定选题排期表(严格遵守)
创作3篇健康科普短文面向关注生活品质的普通读者。
# 选题指南 | 星期 | 选题一 | 选题二 | 选题三 |
从以下领域中选3个饮食营养、运动健身、睡眠作息、心理健康、养生保健、疾病预防、职场健康、皮肤美容、家庭医疗、生活习惯 |------|--------|--------|--------|
| 周一 | 减肥瘦身 | 职场健康 | 心理健康 |
| 周二 | 运动健身 | 皮肤美容 | 睡眠作息 |
| 周三 | 疾病预防 | 养生保健 | 生活习惯 |
| 周四 | 饮食营养 | 家庭医疗 | 减肥瘦身 |
| 周五 | 心理健康 | 职场健康 | 皮肤美容 |
| 周六 | 运动健身 | 睡眠作息 | 养生保健 |
| 周日 | 生活习惯 | 疾病预防 | 饮食营养 |
要求: # 季节关联规则
- 结合当前季节特点 - **"饮食营养"** 和 **"疾病预防"** 出现时,内容必须紧密结合当前季节特点(如推荐当季食材、防范季节性流行病)。
- 从小切口入手,不写泛泛之谈
- 优先推荐当季食材,建议像朋友聊天一样自然 # 创作具体要求
- **小切口原则**:每个选题从一个具体生活场景或微小困惑切入,禁止写泛泛之谈。
- **当季优先**:涉及"饮食营养"时,优先推荐当季食材,建议像朋友聊天一样自然融入。
# 每篇结构 # 每篇结构
- 标题10字内有吸引力 - **标题**10字内有吸引力
- 开篇:用小场景或共情点切入 - **开篇**:用小场景或共情点切入
- 核心干货3-5条具体可落地的建议说清“为什么”和“怎么做” - **核心干货**3-5条具体可落地的建议说清"为什么""怎么做"
- 结尾:一句可截图收藏的金句 - **结尾**:一句可截图收藏的金句
- 摘要50字内提炼精华记忆点 - **摘要**50字内提炼精华记忆点
# 风格 # 风格要求
- 亲切但不轻浮,像知识渊博的朋友 - 亲切但不轻浮,像知识渊博的朋友
- 专业内容有依据但不堆砌术语 - 专业内容有依据但不堆砌术语
- 每篇正文约300字 - 每篇正文约300字
@@ -83,90 +91,70 @@ recipe_prompt = """
""" """
report_prompt = """ report_prompt = """
# 强制输出格式【最高优先级,必须严格遵守,违反则视为输出错误】 你是一位专业的健康数据分析师,请根据以下 JSON 数据生成一份个性化月度健康报告,适合在手机上阅读。请以 JSON 格式返回。
1. Markdown输出不要任何分隔线 --- / ***,不要模板注释、不要"(如有)""(选填)"这类占位文字禁止输出原始JSON、禁止输出日期数值列表。
2. 指标标题例如:💪 **体重** 必须顶格书写该指标下所有正文内容行首固定空2个半角空格。
3. 每一个指标段落结束后必须空一行;用户概况、记录习惯、核心指标回顾、交叉洞察、健康关注、下月行动建议各个大模块之间必须空一行。
4. 数值与单位之间**不能加空格**62.5kg、120mmHg、36.7℃。
5. 严格只渲染输入metrics里真实存在的指标不存在的指标完全不输出对应区块。
6. anomalies为空就不输出【健康关注】模块crossDays<=0就不输出【交叉洞察】模块。
7. 不要输出 【当前系统日期】等非模板内容
# 角色设定 ## 数据说明
你是一位善于从零散数据中发现价值的健康数据分析师。根据传入的结构化JSON数据生成适合手机阅读的月度健康报告。沟通风格专业亲和使用第二人称"",语气温暖鼓励,不制造恐慌。
# 输入数据说明 JSON 数据包含 userInfo用户信息和月度健康数据。
输入为JSON对象字段定义
- user{ age, gender, height, occupation(选填), goal(选填) }
- period报告周期字符串
- summary{ totalDays, indicators(数组), mostFrequent, quality }
- metrics包含 weight, bloodPressure, bloodSugar, temperature, sport, sleep仅部分字段会有有效数据
- crossDays同日多指标记录的天数数字
- anomalies异常记录数组为空代表无异常
# 业务逻辑规则 ### userInfo 字段说明
1. 所有健康解读必须结合 user 的 age、gender、height 信息。 - name用户姓名必有
2. 禁止罗列原始明细数据,不输出一条条日期+数值。 - gender性别必有
3. BMI计算基于用户身高与本月体重数据输出BMI值以及对应健康范围。 - birthYear出生年份必有
4. 血糖区分空腹血糖、餐后2h血糖分别描述存在异常则补充异常提示。 - height身高 cm必有
5. 下月行动建议输出24条必须具体、可量化。 - occupation职业性质"久坐少动"(可能为空)
- goal健康目标"均衡饮食"(可能为空)
- condition疾病史数组可能为空
# 输出完整结构(严格按照下面层级输出,不要增加额外标题) ### 月度数据字段说明
各指标包含totalRecords、startValue、endValue、maxValue、minValue、avgValue、netChange、normalDays/Count、abnormalDays/Count、abnormalRate、maxConsecutiveAbnormal
- weight体重
- sleep睡眠含 qualifiedDays、qualifiedRate
- sport运动含 sportDays、frequency、totalDuration、categoryDistribution、maxConsecutiveNoSport
- bloodPressure血压systolic、diastolic、heartRate
- bloodSugar血糖fasting、premeal、postmeal、bedtime
- temperature体温含 lowFeverDays、highFeverDays
👤 **用户概况** ## 空值处理规则
- occupation 为空 → 不提职业
- goal 为空 → 不提目标
- condition 为空 → 不提疾病史
- 某指标为 null 或 totalRecords 为 0 → 不包含在报告中
- 年龄 · 性别 · 身高 · 报告周期(全部放在同一行,使用空格+圆点分隔) ## 个性化要求
- 根据用户已有信息给出针对性建议
- 参考身高体重计算 BMI 并分析
- 只使用已有信息,不臆测
- 语气亲切,用名字称呼用户
📊 **记录习惯** ## 返回 JSON 格式要求(非常重要)
返回一个 JSON 对象,结构如下:
{
"title": "标题Cxx的健康月报 · 2026年9月",
"greeting": "开篇问候语包含称呼、鼓励语、本月总结、BMI分析等2-4句话",
"sections": [
{
"type": "指标类型weight/sleep/sport/bloodPressure/bloodSugar/temperature",
"emoji": "对应的Emoji😴🏃❤🩸🌡",
"title": "指标标题,如:体重",
"hasAbnormal": true/false,
"data": "指标数据描述,包含数值、变化、趋势等",
"suggestion": "建议内容1-2句话"
}
],
"tips": [
"建议1与数据中最需要改善的指标相关2-3条即可",
"建议2",
"建议3"
],
"ending": "结尾鼓励语1-2句话"
}
- 本月你共记录了 X 天。 ## 格式要求
- 记录覆盖了A、B、C。 - 必须是合法的 JSON 格式
- 记录最多的是 XX。 - 所有文本内容中不要包含换行符 \n,用空格代替
根据summary.quality输出对应评价结论 - 不要使用 Markdown 语法
- 适当使用 Emoji
📈 **核心指标回顾** - 数据用括号简洁说明
- 风格:温暖、亲和
💪 **体重**
- 本月整体呈XX趋势变化幅度 X.Xkg。当前BMI为 XX.X属于XX范围。
❤️ **血压**
- 本月记录 X次收缩压均值 XXXmmHg舒张压均值 XXmmHg整体处于XX状态。
🩸 **血糖**
- 空腹血糖:共 X次均值 X.Xmmol/L在正常范围内。
- 餐后2h血糖共 X次均值 X.Xmmol/L在正常范围内。
(存在异常时追加异常提醒语句,无异常不要写)
🌡️ **体温**
- 记录 X次均值 XX℃整体平稳正常。
🏃 **运动**
- 记录 X次以XX为主单次平均 XX分钟。
😴 **睡眠**
- 记录 X次平均 X.X小时。不足7小时的有 X天。
🔗 **交叉洞察**
- 本月有 X天 同时记录了多个指标。
⚠️ **健康关注**
- 异常内容条目
🎯 **下月行动建议**
- 建议一
- 建议二
- 建议三
📝 **免责声明**
本内容由AI生成仅供参考不替代专业医疗诊断与治疗。
""" """

26
config/database.py Normal file
View File

@@ -0,0 +1,26 @@
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
load_dotenv()
DATABASE_URL = f"mysql+pymysql://root:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:3306/health"
engine = create_engine(url=DATABASE_URL, pool_pre_ping=True, pool_recycle=3600)
# 会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# ORM基类
Base = declarative_base()
def get_db():
# 创建数据库会话实例
db = SessionLocal()
try:
yield db
finally:
db.close()

77
health.sql Normal file
View File

@@ -0,0 +1,77 @@
-- MySQL dump 10.13 Distrib 8.0.27, for Win64 (x86_64)
--
-- Host: 127.0.0.1 Database: health
-- ------------------------------------------------------
-- Server version 8.0.27
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `article`
--
DROP TABLE IF EXISTS `article`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `article` (
`id` bigint NOT NULL,
`date` date NOT NULL COMMENT '日期',
`content` json NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='健康咨询';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `recipe`
--
DROP TABLE IF EXISTS `recipe`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `recipe` (
`id` bigint NOT NULL,
`date` date NOT NULL COMMENT '日期',
`content` json NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='养生食谱';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `report`
--
DROP TABLE IF EXISTS `report`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `report` (
`id` bigint NOT NULL,
`content` json NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='报告';
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-09-04 16:57:35

0
id_generator/__init__.py Normal file
View File

38
id_generator/generator.py Normal file
View File

@@ -0,0 +1,38 @@
"""
雪花算法生成器IdGenerator
"""
# !/usr/bin/python
# coding=UTF-8
from . import options
from . import snowflake_m1
class DefaultIdGenerator:
"""
ID生成器
"""
def __init__(self):
self.snowflake = None
def set_id_generator(self, option: options.IdGeneratorOptions):
"""
设置id生成规则信息
"""
if option.base_time < 100000:
raise ValueError("base time error.")
self.snowflake = snowflake_m1.SnowFlakeM1(option)
def next_id(self) -> int:
"""
获取新的UUID
"""
if self.snowflake is None:
raise ValueError("please set id generator at first.")
return self.snowflake.next_id()

134
id_generator/idregister.py Normal file
View File

@@ -0,0 +1,134 @@
"""
worker id generator
"""
# !/usr/bin/python
# coding=UTF-8
from threading import Thread
import time
import logging
import redis
class Register:
"""
redis封装
- host 代表redis ip
- port 代表redis端口
- max_worker_id worker_id的最大值, 默认为100
- password redis的密码, 默认为空
"""
def __init__(self, host, port, max_worker_id=100, password=None):
self.redis_impl = redis.StrictRedis(host=host, port=port, db=0, password=password)
self.loop_count = 0
self.max_loop_count = 10
self.worker_id_expire_time = 15
self.max_worker_id = max_worker_id
self.worker_id = -1
self.is_stop = False
def get_lock(self, key):
"""
获取分布式全局锁,并设置过期时间为30秒
"""
if self.redis_impl.setnx(key, 1):
self.redis_impl.expire(key, 30)
return True
if self.redis_impl.ttl(key) < 0:
self.redis_impl.expire(key, 30)
return False
def stop(self):
"""
退出注册器的线程
"""
self.is_stop = True
def get_worker_id(self):
"""
获取全局唯一worker_id, 会创建一个线程给worker id续期
失败返回-1
"""
self.loop_count = 0
def extern_life(my_id):
while 1:
time.sleep(self.worker_id_expire_time / 3)
# 是否关闭了
if self.is_stop:
return
# 更新生命周期
if self.worker_id != my_id:
break
try:
self.redis_impl.expire(
f"IdGen:WorkerId:Value:{my_id}",
self.worker_id_expire_time)
except Exception as exe:
logging.error(exe)
continue
self.worker_id = self.__get_next_worker_id()
if self.worker_id > -1:
Thread(target=extern_life, args=[self.worker_id]).start()
return self.worker_id
def __get_next_worker_id(self):
"""
获取全局唯一worker id内部实现
"""
cur = self.redis_impl.incrby("IdGen:WorkerId:Index", 1)
def can_reset():
try:
reset_value = self.redis_impl.incr("IdGen:WorkerId:Value:Edit")
return reset_value != 1
except Exception as ept:
logging.error(ept)
return False
def end_reset():
try:
self.redis_impl.set("IdGen:WorkerId:Value:Edit", 0)
except Exception as ept:
logging.error(ept)
def is_available(worker_id: int):
try:
rst = self.redis_impl.get(f"IdGen:WorkerId:Value:{worker_id}")
return rst != "Y"
except Exception as ept:
logging.error(ept)
return False
if cur > self.max_worker_id:
if can_reset():
self.redis_impl.set("IdGen:WorkerId:Index", -1)
end_reset()
self.loop_count += 1
if self.loop_count > self.max_loop_count:
self.loop_count = 0
return -1
time.sleep(0.2 * self.loop_count)
return self.__get_next_worker_id()
time.sleep(0.2)
return self.__get_next_worker_id()
if is_available(cur):
self.redis_impl.setex(
f"IdGen:WorkerId:Value:{cur}",
self.worker_id_expire_time,
"Y"
)
self.loop_count = 0
return cur
return self.__get_next_worker_id()

43
id_generator/options.py Normal file
View File

@@ -0,0 +1,43 @@
"""
生成器IdGenerator配置选项
"""
# !/usr/bin/python
# coding=UTF-8
class IdGeneratorOptions:
"""
ID生成器配置
- worker_id 全局唯一id, 区分不同uuid生成器实例
- worker_id_bit_length 生成的uuid中worker_id占用的位数
- seq_bit_length 生成的uuid中序列号占用的位数
"""
def __init__(self, worker_id=0, worker_id_bit_length=6, seq_bit_length=6):
# 雪花计算方法,1-漂移算法|2-传统算法), 默认1。目前只实现了1。
self.method = 1
# 基础时间ms单位, 不能超过当前系统时间
self.base_time = 1582136402000
# 机器码, 必须由外部设定, 最大值 2^worker_id_bit_length-1
self.worker_id = worker_id
# 机器码位长, 默认值6, 取值范围 [1, 15](要求:序列数位长+机器码位长不超过22
self.worker_id_bit_length = worker_id_bit_length
# 序列数位长, 默认值6, 取值范围 [3, 21](要求:序列数位长+机器码位长不超过22
self.seq_bit_length = seq_bit_length
# 最大序列数(含), 设置范围 [max_seq_number, 2^seq_bit_length-1]
# 默认值0, 表示最大序列数取最大值2^seq_bit_length-1]
self.max_seq_number = 0
# 最小序列数(含), 默认值5, 取值范围 [5, max_seq_number], 每毫秒的前5个序列数对应编号0-4是保留位
# 其中1-4是时间回拨相应预留位, 0是手工新值预留位
self.min_seq_number = 5
# 最大漂移次数(含), 默认2000, 推荐范围500-10000与计算能力有关
self.top_over_cost_count = 2000

20
id_generator/snowflake.py Normal file
View File

@@ -0,0 +1,20 @@
"""
雪花算法生成器接口声明
"""
# !/usr/bin/python
# coding=UTF-8
class SnowFlake():
def __init__(self, options):
self.options = options
def next_id(self) -> int:
"""
获取新的UUID
"""
return 0

View File

@@ -0,0 +1,147 @@
"""
M1生成器
"""
# !/usr/bin/python
# coding=UTF-8
import threading
import time
from .snowflake import SnowFlake
from .options import IdGeneratorOptions
class SnowFlakeM1(SnowFlake):
"""
M1规则ID生成器配置
"""
def __init__(self, options: IdGeneratorOptions):
# 1.base_time
self.base_time = 1582136402000
if options.base_time != 0:
self.base_time = int(options.base_time)
# 2.worker_id_bit_length
self.worker_id_bit_length = 6
if options.worker_id_bit_length != 0:
self.worker_id_bit_length = int(options.worker_id_bit_length)
# 3.worker_id
self.worker_id = options.worker_id
# 4.seq_bit_length
self.seq_bit_length = 6
if options.seq_bit_length != 0:
self.seq_bit_length = int(options.seq_bit_length)
# 5.max_seq_number
self.max_seq_number = int(options.max_seq_number)
if options.max_seq_number <= 0:
self.max_seq_number = (1 << self.seq_bit_length) - 1
# 6.min_seq_number
self.min_seq_number = int(options.min_seq_number)
# 7.top_over_cost_count
self.top_over_cost_count = int(options.top_over_cost_count)
# 8.Others
self.__timestamp_shift = self.worker_id_bit_length + self.seq_bit_length
self.__current_seq_number = self.min_seq_number
self.__last_time_tick: int = 0
self.__turn_back_time_tick: int = 0
self.__turn_back_index: int = 0
self.__is_over_cost = False
self.___over_cost_count_in_one_term: int = 0
self.__id_lock = threading.Lock()
def __next_over_cost_id(self) -> int:
current_time_tick = self.__get_current_time_tick()
if current_time_tick > self.__last_time_tick:
self.__last_time_tick = current_time_tick
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = False
self.___over_cost_count_in_one_term = 0
return self.__calc_id(self.__last_time_tick)
if self.___over_cost_count_in_one_term >= self.top_over_cost_count:
self.__last_time_tick = self.__get_next_time_tick()
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = False
self.___over_cost_count_in_one_term = 0
return self.__calc_id(self.__last_time_tick)
if self.__current_seq_number > self.max_seq_number:
self.__last_time_tick += 1
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = True
self.___over_cost_count_in_one_term += 1
return self.__calc_id(self.__last_time_tick)
return self.__calc_id(self.__last_time_tick)
def __next_normal_id(self) -> int:
current_time_tick = self.__get_current_time_tick()
if current_time_tick < self.__last_time_tick:
if self.__turn_back_time_tick < 1:
self.__turn_back_time_tick = self.__last_time_tick - 1
self.__turn_back_index += 1
# 每毫秒序列数的前5位是预留位, 0用于手工新值, 1-4是时间回拨次序
# 支持4次回拨次序避免回拨重叠导致ID重复, 可无限次回拨(次序循环使用)。
if self.__turn_back_index > 4:
self.__turn_back_index = 1
return self.__calc_turn_back_id(self.__turn_back_time_tick)
# 时间追平时, _TurnBackTimeTick清零
self.__turn_back_time_tick = min(self.__turn_back_time_tick, 0)
if current_time_tick > self.__last_time_tick:
self.__last_time_tick = current_time_tick
self.__current_seq_number = self.min_seq_number
return self.__calc_id(self.__last_time_tick)
if self.__current_seq_number > self.max_seq_number:
self.__last_time_tick += 1
self.__current_seq_number = self.min_seq_number
self.__is_over_cost = True
self.___over_cost_count_in_one_term = 1
return self.__calc_id(self.__last_time_tick)
return self.__calc_id(self.__last_time_tick)
def __calc_id(self, use_time_tick) -> int:
self.__current_seq_number += 1
return (
(use_time_tick << self.__timestamp_shift) +
(self.worker_id << self.seq_bit_length) +
self.__current_seq_number
) % int(1e64)
def __calc_turn_back_id(self, use_time_tick) -> int:
self.__turn_back_time_tick -= 1
return (
(use_time_tick << self.__timestamp_shift) +
(self.worker_id << self.seq_bit_length) +
self.__turn_back_index
) % int(1e64)
def __get_current_time_tick(self) -> int:
return int((time.time_ns() / 1e6) - self.base_time)
def __get_next_time_tick(self) -> int:
temp_time_ticker = self.__get_current_time_tick()
while temp_time_ticker <= self.__last_time_tick:
# 0.001 = 1 mili sec
time.sleep(0.001)
temp_time_ticker = self.__get_current_time_tick()
return temp_time_ticker
def next_id(self) -> int:
with self.__id_lock:
if self.__is_over_cost:
nextid = self.__next_over_cost_id()
else:
nextid = self.__next_normal_id()
return nextid

View File

@@ -5,17 +5,18 @@ from apscheduler.schedulers.background import BackgroundScheduler
from fastapi import FastAPI from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from config.database import get_db
from routers import routers from routers import routers
from service import agent_service from service import agent_service
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
def tip_job_task(): def article_job_task():
"""定时执行的任务""" """定时执行的任务"""
try: try:
print(f"今日资讯定时任务执行: {datetime.now()}") print(f"今日资讯定时任务执行: {datetime.now()}")
agent_service.query_tip_agent() agent_service.query_article_agent(get_db())
except Exception as e: except Exception as e:
print(f"今日资讯定时任务异常: {e}") print(f"今日资讯定时任务异常: {e}")
@@ -23,12 +24,12 @@ def recipe_job_task():
"""定时执行的任务""" """定时执行的任务"""
try: try:
print(f"今日菜谱定时任务执行: {datetime.now()}") print(f"今日菜谱定时任务执行: {datetime.now()}")
agent_service.query_recipe_agent() agent_service.query_recipe_agent(get_db())
except Exception as e: except Exception as e:
print(f"今日菜谱定时任务异常: {e}") print(f"今日菜谱定时任务异常: {e}")
scheduler.add_job(tip_job_task, trigger="cron", hour=2, minute=0, second=0, id="daily_gen_tips", replace_existing=True) scheduler.add_job(article_job_task, trigger="cron", hour=2, minute=0, second=0, id="daily_gen_articles", replace_existing=True)
scheduler.add_job(recipe_job_task, trigger="cron", hour=3, minute=0, second=0, id="daily_gen_recipes", replace_existing=True) scheduler.add_job(recipe_job_task, trigger="cron", hour=3, minute=0, second=0, id="daily_gen_recipes", replace_existing=True)
@asynccontextmanager @asynccontextmanager

41
models/base.py Normal file
View File

@@ -0,0 +1,41 @@
from sqlalchemy import Column, BigInteger, DateTime, event
from sqlalchemy.ext.declarative import declared_attr
from config.database import Base
from datetime import datetime
from utils.common import camel_to_snake
from id_generator import options, generator
# https://github.com/yitter/IdGenerator/tree/master/Python
options = options.IdGeneratorOptions(worker_id=23)
idgen = generator.DefaultIdGenerator()
idgen.set_id_generator(options)
# 第二层基类包含ID
class IdBase(Base):
__abstract__ = True
id = Column(BigInteger, primary_key=True, index=True)
@declared_attr
def __tablename__(cls):
# 自动把数据库实体类名驼峰转为数据库表名下划线
return camel_to_snake(cls.__name__)
# 自动填充id
@event.listens_for(IdBase, 'before_insert', propagate=True)
def before_insert_listener(mapper, connection, target):
if target.id is None:
target.id = idgen.next_id()
# 第二层基类包含ID和审计字段
class AuditBase(IdBase):
__abstract__ = True
create_time = Column(DateTime, nullable=True, default=datetime.now)
update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now)

16
models/health.py Normal file
View File

@@ -0,0 +1,16 @@
from sqlalchemy import (Column, Date, JSON)
from models.base import AuditBase
class Article(AuditBase):
date = Column(Date, nullable=False, comment="日期")
content = Column(JSON, nullable=False, default=dict, comment="内容")
class Recipe(AuditBase):
date = Column(Date, nullable=False, comment="日期")
content = Column(JSON, nullable=False, default=dict, comment="内容")
class Report(AuditBase):
content = Column(JSON, nullable=False, default=dict, comment="内容")

View File

@@ -10,3 +10,4 @@ SQLAlchemy~=2.0.51
asyncpg~=0.30.0 asyncpg~=0.30.0
uvicorn~=0.23.0 uvicorn~=0.23.0
APScheduler~=3.11.3 APScheduler~=3.11.3
pymysql~=1.2.0

View File

@@ -1,5 +1,7 @@
from .agent import router as agent_router from .agent import router as agent_router
from .report import router as report_router
routers = [ routers = [
agent_router agent_router,
report_router
] ]

View File

@@ -1,31 +1,33 @@
from fastapi import APIRouter from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from schema.agent import ReportRequest from config.database import get_db
from schema.agent import ReportAgentRequest
from service import agent_service from service import agent_service
router = APIRouter(prefix="/agent", tags=["Agent"]) router = APIRouter(prefix="/agent", tags=["Agent"])
@router.post("/tip/generate") @router.post("/article/generate")
def generate_tip(): def generate_article(db: Session = Depends(get_db)):
return agent_service.query_tip_agent() return agent_service.query_article_agent(db)
@router.get("/tip/latest") @router.get("/article/latest")
def latest_tip(): def latest_article(db: Session = Depends(get_db)):
return agent_service.get_latest_tip() return agent_service.get_latest_article(db)
@router.post("/recipe/generate") @router.post("/recipe/generate")
def generate_recipe(): def generate_recipe(db: Session = Depends(get_db)):
return agent_service.query_recipe_agent() return agent_service.query_recipe_agent(db)
@router.get("/recipe/latest") @router.get("/recipe/latest")
def latest_recipe(): def latest_recipe(db: Session = Depends(get_db)):
return agent_service.get_latest_recipe() return agent_service.get_latest_recipe(db)
@router.post("/report") @router.post("/report")
def query_report(req: ReportRequest): def query_report(req: ReportAgentRequest):
return agent_service.query_report_agent(req) return agent_service.query_report_agent(req)

18
routers/report.py Normal file
View File

@@ -0,0 +1,18 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from config.database import get_db
from schema.report import ReportRequest
from service import report_service
router = APIRouter(prefix="/report", tags=["Report"])
@router.get("/{id}")
def query_report(id: int, db: Session = Depends(get_db)):
return report_service.query_report(id, db)
@router.post("")
def upload_report(req: ReportRequest, db: Session = Depends(get_db)):
return report_service.upload_report(req.content, db)

View File

@@ -1,5 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
class ReportRequest(BaseModel): class ReportAgentRequest(BaseModel):
query: str query: str

5
schema/report.py Normal file
View File

@@ -0,0 +1,5 @@
from pydantic import BaseModel
class ReportRequest(BaseModel):
content: str

View File

@@ -1,52 +1,62 @@
import json import json
from datetime import datetime from datetime import datetime, date
from typing import Dict, Any
from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.messages import HumanMessage, SystemMessage
from sqlalchemy import select, delete
from sqlalchemy.orm import Session
from agent.health import tip_agent, recipe_agent, report_agent from agent.health import article_agent, recipe_agent, report_agent
from schema.agent import ReportRequest from models.health import Article, Recipe
from schema.agent import ReportAgentRequest
memory_store: Dict[str, Any] = {
"articles": [],
"recipe": []
}
def query_tip_agent(): def query_article_agent(db: Session):
try: try:
print("开始生成今日健康资讯") print("开始生成今日健康资讯")
today = datetime.today().strftime("%Y-%m-%d") today = datetime.today().strftime("%Y-%m-%d")
resp = tip_agent.invoke({ resp = article_agent.invoke({
"messages": [SystemMessage(content=f"当前系统日期:{today} \n\n"), "messages": [SystemMessage(content=f"当前系统日期:{today} \n\n"),
HumanMessage(content="请生成今日健康资讯")] HumanMessage(content="请生成今日健康资讯")]
}) })
last_msg = resp["messages"][-1] last_msg = resp["messages"][-1]
raw_text = last_msg.content.strip() raw_text = last_msg.content.strip()
# 解析大模型返回的json字符串 db.execute(
articles = json.loads(raw_text) delete(Article).where(Article.date == today)
# 写入内存缓存 )
memory_store["articles"] = articles
article = Article(
date=date.today(),
content=json.loads(raw_text),
)
db.add(article)
db.commit()
db.refresh(article)
print("结束生成今日健康资讯") print("结束生成今日健康资讯")
return True return True
except json.JSONDecodeError as je: except json.JSONDecodeError as je:
print(f"[JSON解析错误] {str(je)}") print(f"[JSON解析错误] {str(je)}")
memory_store["articles"] = []
return False return False
except Exception as e: except Exception as e:
print(f"\n[错误]: {str(e)}") print(f"\n[错误]: {str(e)}")
memory_store["articles"] = []
return False return False
def get_latest_tip(): def get_latest_article(db: Session):
return memory_store["articles"] today = date.today()
result = db.execute(
select(Article).where(Article.date == today)
)
article = result.scalar_one_or_none()
return article
def query_recipe_agent(): def query_recipe_agent(db: Session):
try: try:
print("开始生成今日养生食谱") print("开始生成今日养生食谱")
today = datetime.today().strftime("%Y-%m-%d") today = datetime.today().strftime("%Y-%m-%d")
@@ -58,28 +68,41 @@ def query_recipe_agent():
last_msg = resp["messages"][-1] last_msg = resp["messages"][-1]
raw_text = last_msg.content.strip() raw_text = last_msg.content.strip()
# 解析大模型返回的json字符串 db.execute(
recipes = json.loads(raw_text) delete(Recipe).where(Recipe.date == today)
# 写入内存缓存 )
memory_store["recipe"] = recipes
recipe = Recipe(
date=date.today(),
content=json.loads(raw_text),
)
db.add(recipe)
db.commit()
db.refresh(recipe)
print("结束生成今日养生食谱") print("结束生成今日养生食谱")
return True return True
except json.JSONDecodeError as je: except json.JSONDecodeError as je:
print(f"[JSON解析错误] {str(je)}") print(f"[JSON解析错误] {str(je)}")
memory_store["recipe"] = []
return False return False
except Exception as e: except Exception as e:
print(f"\n[错误]: {str(e)}") print(f"\n[错误]: {str(e)}")
memory_store["recipe"] = []
return False return False
def get_latest_recipe(): def get_latest_recipe(db: Session):
return memory_store["recipe"] today = date.today()
result = db.execute(
select(Recipe).where(Recipe.date == today)
)
article = result.scalar_one_or_none()
return article
def query_report_agent(req: ReportRequest): def query_report_agent(req: ReportAgentRequest):
try: try:
print("开始生成报告") print("开始生成报告")
today = datetime.today().strftime("%Y-%m-%d") today = datetime.today().strftime("%Y-%m-%d")

24
service/report_service.py Normal file
View File

@@ -0,0 +1,24 @@
import json
from sqlalchemy import select
from sqlalchemy.orm import Session
from models.health import Report
def upload_report(report: str, db: Session):
report = Report(
content=json.loads(report),
)
db.add(report)
db.commit()
db.refresh(report)
return True
def query_report(id: int, db: Session):
result = db.execute(
select(Report).where(Report.id == id)
)
return result.scalar_one_or_none()

13
utils/common.py Normal file
View File

@@ -0,0 +1,13 @@
import re
def camel_to_snake(name: str) -> str:
"""将驼峰命名转换为蛇形命名CamelCase → snake_case"""
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
def snake_to_camel(name: str) -> str:
"""将蛇形命名转换为驼峰命名snake_case → CamelCase"""
components = name.split('_')
return ''.join(x.title() for x in components)