feat: 接入mysql数据库
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
from langchain.agents import create_agent
|
||||
|
||||
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,
|
||||
system_prompt=tip_prompt
|
||||
system_prompt=article_prompt
|
||||
)
|
||||
|
||||
recipe_agent = create_agent(
|
||||
|
||||
174
agent/prompt.py
174
agent/prompt.py
@@ -1,26 +1,34 @@
|
||||
tip_prompt = """
|
||||
article_prompt = """
|
||||
# 角色设定
|
||||
你是一位兼具科学素养与人文关怀的健康科普作家。文字温暖亲切,擅长把复杂医学知识转化为生活智慧。
|
||||
|
||||
# 创作任务
|
||||
创作3篇健康科普短文,面向关注生活品质的普通读者。
|
||||
# 每日固定选题排期表(严格遵守)
|
||||
|
||||
# 选题指南
|
||||
从以下领域中选3个:饮食营养、运动健身、睡眠作息、心理健康、养生保健、疾病预防、职场健康、皮肤美容、家庭医疗、生活习惯
|
||||
| 星期 | 选题一 | 选题二 | 选题三 |
|
||||
|------|--------|--------|--------|
|
||||
| 周一 | 减肥瘦身 | 职场健康 | 心理健康 |
|
||||
| 周二 | 运动健身 | 皮肤美容 | 睡眠作息 |
|
||||
| 周三 | 疾病预防 | 养生保健 | 生活习惯 |
|
||||
| 周四 | 饮食营养 | 家庭医疗 | 减肥瘦身 |
|
||||
| 周五 | 心理健康 | 职场健康 | 皮肤美容 |
|
||||
| 周六 | 运动健身 | 睡眠作息 | 养生保健 |
|
||||
| 周日 | 生活习惯 | 疾病预防 | 饮食营养 |
|
||||
|
||||
要求:
|
||||
- 结合当前季节特点
|
||||
- 从小切口入手,不写泛泛之谈
|
||||
- 优先推荐当季食材,建议像朋友聊天一样自然
|
||||
# 季节关联规则
|
||||
- **"饮食营养"** 和 **"疾病预防"** 出现时,内容必须紧密结合当前季节特点(如推荐当季食材、防范季节性流行病)。
|
||||
|
||||
# 创作具体要求
|
||||
- **小切口原则**:每个选题从一个具体生活场景或微小困惑切入,禁止写泛泛之谈。
|
||||
- **当季优先**:涉及"饮食营养"时,优先推荐当季食材,建议像朋友聊天一样自然融入。
|
||||
|
||||
# 每篇结构
|
||||
- 标题:10字内,有吸引力
|
||||
- 开篇:用小场景或共情点切入
|
||||
- 核心干货:3-5条具体可落地的建议,说清“为什么”和“怎么做”
|
||||
- 结尾:一句可截图收藏的金句
|
||||
- 摘要:50字内,提炼精华记忆点
|
||||
- **标题**:10字内,有吸引力
|
||||
- **开篇**:用小场景或共情点切入
|
||||
- **核心干货**:3-5条具体可落地的建议,说清"为什么"和"怎么做"
|
||||
- **结尾**:一句可截图收藏的金句
|
||||
- **摘要**:50字内,提炼精华记忆点
|
||||
|
||||
# 风格
|
||||
# 风格要求
|
||||
- 亲切但不轻浮,像知识渊博的朋友
|
||||
- 专业内容有依据但不堆砌术语
|
||||
- 每篇正文约300字
|
||||
@@ -83,90 +91,70 @@ recipe_prompt = """
|
||||
"""
|
||||
|
||||
report_prompt = """
|
||||
# 强制输出格式【最高优先级,必须严格遵守,违反则视为输出错误】
|
||||
1. Markdown输出,不要任何分隔线 --- / ***,不要模板注释、不要"(如有)""(选填)"这类占位文字,禁止输出原始JSON、禁止输出日期数值列表。
|
||||
2. 指标标题例如:💪 **体重** 必须顶格书写;该指标下所有正文内容行首固定空2个半角空格。
|
||||
3. 每一个指标段落结束后必须空一行;用户概况、记录习惯、核心指标回顾、交叉洞察、健康关注、下月行动建议各个大模块之间必须空一行。
|
||||
4. 数值与单位之间**不能加空格**,例:62.5kg、120mmHg、36.7℃。
|
||||
5. 严格只渲染输入metrics里真实存在的指标,不存在的指标完全不输出对应区块。
|
||||
6. anomalies为空就不输出【健康关注】模块;crossDays<=0就不输出【交叉洞察】模块。
|
||||
7. 不要输出 【当前系统日期】等非模板内容
|
||||
你是一位专业的健康数据分析师,请根据以下 JSON 数据生成一份个性化月度健康报告,适合在手机上阅读。请以 JSON 格式返回。
|
||||
|
||||
# 角色设定
|
||||
你是一位善于从零散数据中发现价值的健康数据分析师。根据传入的结构化JSON数据生成适合手机阅读的月度健康报告。沟通风格专业亲和,使用第二人称"你",语气温暖鼓励,不制造恐慌。
|
||||
## 数据说明
|
||||
|
||||
# 输入数据说明
|
||||
输入为JSON对象,字段定义:
|
||||
- user:{ age, gender, height, occupation(选填), goal(选填) }
|
||||
- period:报告周期字符串
|
||||
- summary:{ totalDays, indicators(数组), mostFrequent, quality }
|
||||
- metrics:包含 weight, bloodPressure, bloodSugar, temperature, sport, sleep,仅部分字段会有有效数据
|
||||
- crossDays:同日多指标记录的天数,数字
|
||||
- anomalies:异常记录数组,为空代表无异常
|
||||
JSON 数据包含 userInfo(用户信息)和月度健康数据。
|
||||
|
||||
# 业务逻辑规则
|
||||
1. 所有健康解读必须结合 user 的 age、gender、height 信息。
|
||||
2. 禁止罗列原始明细数据,不输出一条条日期+数值。
|
||||
3. BMI计算基于用户身高与本月体重数据,输出BMI值以及对应健康范围。
|
||||
4. 血糖区分空腹血糖、餐后2h血糖分别描述;存在异常则补充异常提示。
|
||||
5. 下月行动建议输出2‑4条,必须具体、可量化。
|
||||
### userInfo 字段说明
|
||||
- name:用户姓名(必有)
|
||||
- gender:性别(必有)
|
||||
- birthYear:出生年份(必有)
|
||||
- height:身高 cm(必有)
|
||||
- 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。
|
||||
- 记录最多的是 XX。
|
||||
(根据summary.quality输出对应评价结论)
|
||||
|
||||
📈 **核心指标回顾**
|
||||
|
||||
💪 **体重**
|
||||
|
||||
- 本月整体呈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生成,仅供参考,不替代专业医疗诊断与治疗。
|
||||
## 格式要求
|
||||
- 必须是合法的 JSON 格式
|
||||
- 所有文本内容中不要包含换行符 \n,用空格代替
|
||||
- 不要使用 Markdown 语法
|
||||
- 适当使用 Emoji
|
||||
- 数据用括号简洁说明
|
||||
- 风格:温暖、亲和
|
||||
"""
|
||||
|
||||
26
config/database.py
Normal file
26
config/database.py
Normal 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
77
health.sql
Normal 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
0
id_generator/__init__.py
Normal file
38
id_generator/generator.py
Normal file
38
id_generator/generator.py
Normal 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
134
id_generator/idregister.py
Normal 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
43
id_generator/options.py
Normal 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
20
id_generator/snowflake.py
Normal 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
|
||||
147
id_generator/snowflake_m1.py
Normal file
147
id_generator/snowflake_m1.py
Normal 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
|
||||
9
main.py
9
main.py
@@ -5,17 +5,18 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from config.database import get_db
|
||||
from routers import routers
|
||||
from service import agent_service
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
|
||||
def tip_job_task():
|
||||
def article_job_task():
|
||||
"""定时执行的任务"""
|
||||
try:
|
||||
print(f"今日资讯定时任务执行: {datetime.now()}")
|
||||
agent_service.query_tip_agent()
|
||||
agent_service.query_article_agent(get_db())
|
||||
except Exception as e:
|
||||
print(f"今日资讯定时任务异常: {e}")
|
||||
|
||||
@@ -23,12 +24,12 @@ def recipe_job_task():
|
||||
"""定时执行的任务"""
|
||||
try:
|
||||
print(f"今日菜谱定时任务执行: {datetime.now()}")
|
||||
agent_service.query_recipe_agent()
|
||||
agent_service.query_recipe_agent(get_db())
|
||||
except Exception as 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)
|
||||
|
||||
@asynccontextmanager
|
||||
|
||||
41
models/base.py
Normal file
41
models/base.py
Normal 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
16
models/health.py
Normal 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="内容")
|
||||
@@ -10,3 +10,4 @@ SQLAlchemy~=2.0.51
|
||||
asyncpg~=0.30.0
|
||||
uvicorn~=0.23.0
|
||||
APScheduler~=3.11.3
|
||||
pymysql~=1.2.0
|
||||
@@ -1,5 +1,7 @@
|
||||
from .agent import router as agent_router
|
||||
from .report import router as report_router
|
||||
|
||||
routers = [
|
||||
agent_router
|
||||
agent_router,
|
||||
report_router
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
router = APIRouter(prefix="/agent", tags=["Agent"])
|
||||
|
||||
|
||||
@router.post("/tip/generate")
|
||||
def generate_tip():
|
||||
return agent_service.query_tip_agent()
|
||||
@router.post("/article/generate")
|
||||
def generate_article(db: Session = Depends(get_db)):
|
||||
return agent_service.query_article_agent(db)
|
||||
|
||||
|
||||
@router.get("/tip/latest")
|
||||
def latest_tip():
|
||||
return agent_service.get_latest_tip()
|
||||
@router.get("/article/latest")
|
||||
def latest_article(db: Session = Depends(get_db)):
|
||||
return agent_service.get_latest_article(db)
|
||||
|
||||
|
||||
@router.post("/recipe/generate")
|
||||
def generate_recipe():
|
||||
return agent_service.query_recipe_agent()
|
||||
def generate_recipe(db: Session = Depends(get_db)):
|
||||
return agent_service.query_recipe_agent(db)
|
||||
|
||||
|
||||
@router.get("/recipe/latest")
|
||||
def latest_recipe():
|
||||
return agent_service.get_latest_recipe()
|
||||
def latest_recipe(db: Session = Depends(get_db)):
|
||||
return agent_service.get_latest_recipe(db)
|
||||
|
||||
|
||||
@router.post("/report")
|
||||
def query_report(req: ReportRequest):
|
||||
def query_report(req: ReportAgentRequest):
|
||||
return agent_service.query_report_agent(req)
|
||||
|
||||
18
routers/report.py
Normal file
18
routers/report.py
Normal 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)
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ReportRequest(BaseModel):
|
||||
class ReportAgentRequest(BaseModel):
|
||||
query: str
|
||||
5
schema/report.py
Normal file
5
schema/report.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ReportRequest(BaseModel):
|
||||
content: str
|
||||
@@ -1,52 +1,62 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime, date
|
||||
|
||||
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 schema.agent import ReportRequest
|
||||
|
||||
memory_store: Dict[str, Any] = {
|
||||
"articles": [],
|
||||
"recipe": []
|
||||
}
|
||||
from agent.health import article_agent, recipe_agent, report_agent
|
||||
from models.health import Article, Recipe
|
||||
from schema.agent import ReportAgentRequest
|
||||
|
||||
|
||||
def query_tip_agent():
|
||||
def query_article_agent(db: Session):
|
||||
try:
|
||||
print("开始生成今日健康资讯")
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
|
||||
resp = tip_agent.invoke({
|
||||
resp = article_agent.invoke({
|
||||
"messages": [SystemMessage(content=f"当前系统日期:{today} \n\n"),
|
||||
HumanMessage(content="请生成今日健康资讯")]
|
||||
})
|
||||
last_msg = resp["messages"][-1]
|
||||
raw_text = last_msg.content.strip()
|
||||
|
||||
# 解析大模型返回的json字符串
|
||||
articles = json.loads(raw_text)
|
||||
# 写入内存缓存
|
||||
memory_store["articles"] = articles
|
||||
db.execute(
|
||||
delete(Article).where(Article.date == today)
|
||||
)
|
||||
|
||||
article = Article(
|
||||
date=date.today(),
|
||||
content=json.loads(raw_text),
|
||||
)
|
||||
db.add(article)
|
||||
db.commit()
|
||||
db.refresh(article)
|
||||
|
||||
print("结束生成今日健康资讯")
|
||||
return True
|
||||
except json.JSONDecodeError as je:
|
||||
print(f"[JSON解析错误] {str(je)}")
|
||||
memory_store["articles"] = []
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[错误]: {str(e)}")
|
||||
memory_store["articles"] = []
|
||||
return False
|
||||
|
||||
|
||||
def get_latest_tip():
|
||||
return memory_store["articles"]
|
||||
def get_latest_article(db: Session):
|
||||
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:
|
||||
print("开始生成今日养生食谱")
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
@@ -58,28 +68,41 @@ def query_recipe_agent():
|
||||
last_msg = resp["messages"][-1]
|
||||
raw_text = last_msg.content.strip()
|
||||
|
||||
# 解析大模型返回的json字符串
|
||||
recipes = json.loads(raw_text)
|
||||
# 写入内存缓存
|
||||
memory_store["recipe"] = recipes
|
||||
db.execute(
|
||||
delete(Recipe).where(Recipe.date == today)
|
||||
)
|
||||
|
||||
recipe = Recipe(
|
||||
date=date.today(),
|
||||
content=json.loads(raw_text),
|
||||
)
|
||||
db.add(recipe)
|
||||
db.commit()
|
||||
db.refresh(recipe)
|
||||
|
||||
print("结束生成今日养生食谱")
|
||||
return True
|
||||
except json.JSONDecodeError as je:
|
||||
print(f"[JSON解析错误] {str(je)}")
|
||||
memory_store["recipe"] = []
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[错误]: {str(e)}")
|
||||
memory_store["recipe"] = []
|
||||
return False
|
||||
|
||||
|
||||
def get_latest_recipe():
|
||||
return memory_store["recipe"]
|
||||
def get_latest_recipe(db: Session):
|
||||
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:
|
||||
print("开始生成报告")
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
|
||||
24
service/report_service.py
Normal file
24
service/report_service.py
Normal 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
13
utils/common.py
Normal 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)
|
||||
Reference in New Issue
Block a user