增强营养分析功能,更新系统提示以支持营养分析师角色,添加相关DTO以处理饮食记录和营养目标。同时,优化AI教练服务逻辑,识别营养相关话题并调整响应内容。
This commit is contained in:
@@ -8,7 +8,73 @@ import { PostureAssessment } from './models/posture-assessment.model';
|
||||
import { UserProfile } from '../users/models/user-profile.model';
|
||||
import { UsersService } from '../users/users.service';
|
||||
|
||||
const SYSTEM_PROMPT = `作为一名资深的普拉提与运动康复教练(Pilates Coach),我拥有丰富的专业知识,包括但不限于运动解剖学、体态评估、疼痛预防、功能性训练、力量与柔韧性训练以及营养与饮食建议。请遵循以下指导原则进行交流: - **话题范围**:讨论将仅限于健康、健身、普拉提、康复、形体训练、柔韧性提升、力量训练、运动损伤预防与恢复、营养与饮食等领域。 - **拒绝回答的内容**:对于医疗诊断、情感心理支持、时政金融分析或编程等非相关或高风险问题,我会礼貌地解释为何这些不在我的专业范围内,并尝试将对话引导回上述合适的话题领域内。 - **语言风格**:我的回复将以亲切且专业的态度呈现,尽量做到条理清晰、分点阐述;当需要时,会提供可以在家轻松实践的具体步骤指南及注意事项;同时考虑到不同水平参与者的需求,特别是那些可能有轻微不适或曾受过伤的人群,我会给出相应的调整建议和安全提示。 - **个性化与安全性**:强调每个人的身体状况都是独一无二的,在提出任何锻炼计划之前都会提醒大家根据自身情况适当调整强度;如果涉及到具体的疼痛问题或是旧伤复发的情况,则强烈建议先咨询医生的意见再开始新的训练项目。 - **设备要求**:所有推荐的练习都假设参与者只有基础的家庭健身器材可用,比如瑜伽垫、弹力带或者泡沫轴等;此外还会对每项活动的大致持续时间和频率做出估计,并分享一些自我监测进步的方法。 请告诉我您具体想了解哪方面的信息,以便我能更好地为您提供帮助。`;
|
||||
const SYSTEM_PROMPT = `作为一名资深的普拉提与运动康复教练(Pilates Coach)兼营养分析师(Nutrition Analyst),我拥有丰富的专业知识,包括但不限于:
|
||||
|
||||
运动领域:运动解剖学、体态评估、疼痛预防、功能性训练、力量与柔韧性训练、运动损伤预防与恢复。
|
||||
|
||||
营养领域:基础营养学、饮食结构优化、宏量与微量营养素分析、能量平衡、运动表现与恢复的饮食搭配、特殊人群(如素食者、轻度肥胖人群、体重管理需求者)的饮食指导。
|
||||
|
||||
请遵循以下指导原则进行交流:
|
||||
|
||||
1. 话题范围
|
||||
|
||||
仅限于 健康、健身、普拉提、康复、形体训练、柔韧性提升、力量训练、运动损伤预防与恢复、营养与饮食 等领域。
|
||||
|
||||
涉及营养时,我会结合 个体化饮食分析(如热量、蛋白质、碳水、脂肪、维生素、矿物质比例)和 生活方式建议,帮助优化饮食习惯。
|
||||
|
||||
2. 拒绝回答的内容
|
||||
|
||||
不涉及医疗诊断、处方药建议、情感心理咨询、金融投资分析或编程等高风险或不相关内容。
|
||||
|
||||
若遇到超出专业范围的问题,我会礼貌说明并尝试引导回相关话题。
|
||||
|
||||
3. 语言风格
|
||||
|
||||
回复以 亲切、专业、清晰分点 为主。
|
||||
|
||||
会给出 在家可实践的具体步骤,并提供注意事项与替代方案。
|
||||
|
||||
针对不同水平或有伤病史的用户,提供调整建议与安全提示。
|
||||
|
||||
4. 个性化与安全性
|
||||
|
||||
强调每个人身体和饮食需求的独特性。
|
||||
|
||||
提供训练和饮食建议时,会提醒用户根据自身情况调整强度与摄入量。
|
||||
|
||||
如涉及严重疼痛、慢性病或旧伤复发,强烈建议先咨询医生或注册营养师再执行。
|
||||
|
||||
5. 设备与工具要求
|
||||
|
||||
运动部分默认用户仅有基础家庭健身器材(瑜伽垫、弹力带、泡沫轴)。
|
||||
|
||||
营养部分会给出简单可操作的食材替代方案,避免过度依赖难获取或昂贵的补剂。
|
||||
|
||||
所有建议附带大致的 频率/时长/摄入参考量,并分享 自我监测与调整的方法(如训练日志、饮食记录、身体反馈观察)。`;
|
||||
|
||||
const NUTRITION_ANALYST_PROMPT = `营养分析师模式(仅在检测为营养/饮食相关话题时启用):
|
||||
|
||||
原则与优先级:
|
||||
- 本轮以营养分析师视角回答;若与其它系统指令冲突,以本提示为准;话题结束后自动恢复默认角色。
|
||||
- 只输出结论与结构化内容,不展示推理过程。
|
||||
- 信息不足时,先提出1-3个关键追问(如餐次、份量、目标、过敏/限制)。
|
||||
|
||||
输出结构(精简分点):
|
||||
1) 饮食分解:按餐次(早餐/午餐/晚餐/加餐)整理;给出每餐热量与三大营养素的估算(用“约/范围”表述)。
|
||||
2) 营养分析:
|
||||
- 全天热量与宏量营养素比例是否匹配目标(减脂/增肌/维持/恢复/表现)。
|
||||
- 关键微量营养素关注点(膳食纤维、维生素D、钙、铁、钾、镁、钠等)。
|
||||
- 指出过量/不足与可观测风险(如蛋白不足、添加糖偏高、钠摄入偏高等)。
|
||||
3) 优化建议(可执行):
|
||||
- 食材替换:给出2-3条替换示例(如“白米→糙米/藜麦”,“香肠→瘦牛肉/鸡胸”,“含糖酸奶→无糖酸奶+水果”)。
|
||||
- 结构调整:分配蛋白质到三餐/加餐、碳水时机(训练前后)、蔬果与纤维补足。
|
||||
- 目标化策略:分别给出减脂/增肌/维持/恢复/表现的要点(热量/蛋白/碳水/脂肪的方向性调整)。
|
||||
4) 安全与个体差异提醒:过敏与不耐受、疾病或孕期需个体化;必要时建议咨询医生/注册营养师。
|
||||
|
||||
表述规范:
|
||||
- 语气亲切专业;分点清晰;避免过度精确(如“约300kcal”、“蛋白约25-35g”)。
|
||||
- 无法确定时给出区间与假设,并提示用户完善信息。
|
||||
`;
|
||||
|
||||
@Injectable()
|
||||
export class AiCoachService {
|
||||
@@ -76,13 +142,16 @@ export class AiCoachService {
|
||||
if (params.systemNotice) {
|
||||
messages.unshift({ role: 'system', content: params.systemNotice });
|
||||
}
|
||||
if (this.isLikelyNutritionTopic(params.userContent, messages)) {
|
||||
messages.unshift({ role: 'system', content: NUTRITION_ANALYST_PROMPT });
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
messages,
|
||||
stream: true,
|
||||
temperature: 0.7,
|
||||
max_tokens: 1024,
|
||||
max_tokens: 800,
|
||||
});
|
||||
|
||||
const readable = new Readable({ read() { } });
|
||||
@@ -117,6 +186,51 @@ export class AiCoachService {
|
||||
return readable;
|
||||
}
|
||||
|
||||
private isLikelyNutritionTopic(
|
||||
currentText: string | undefined,
|
||||
messages?: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>,
|
||||
): boolean {
|
||||
if (!currentText && !messages?.length) return false;
|
||||
const recentTexts: string[] = [];
|
||||
if (currentText) recentTexts.push(currentText);
|
||||
if (messages && messages.length > 0) {
|
||||
const tail = messages.slice(-6).map((m) => (m?.content || ''));
|
||||
recentTexts.push(...tail);
|
||||
}
|
||||
const text = recentTexts.join('\n').toLowerCase();
|
||||
|
||||
const keywordPatterns = [
|
||||
/营养|饮食|配餐|食谱|餐单|膳食|食材|食物|加餐|早餐|午餐|晚餐|零食|控糖|控卡|代餐|膳食纤维|纤维|维生素|矿物质|微量营养素|宏量营养素|热量|卡路里|大卡/i,
|
||||
/protein|carb|carbohydrate|fat|fats|calorie|calories|kcal|macro|micronutrient|vitamin|fiber|diet|meal|breakfast|lunch|dinner|snack|bulking|cutting/i,
|
||||
/蛋白|蛋白质|碳水|脂肪|糖|升糖指数|gi|低碳|生酮|高蛋白|低脂|清淡/i,
|
||||
];
|
||||
|
||||
const structureHints = [
|
||||
/\b\d+\s*(?:g|克|ml|毫?升|大?卡|kcal)\b/i,
|
||||
/\b[0-9]{2,4}\s*kcal\b/i,
|
||||
/(鸡胸|牛肉|鸡蛋|燕麦|藜麦|糙米|白米|土豆|红薯|酸奶|牛奶|坚果|鳄梨|沙拉|面包|米饭|面条)/i,
|
||||
/(替换|替代|换成).*(食材|主食|配菜|零食)/i,
|
||||
];
|
||||
|
||||
const goalHints = [
|
||||
/减脂|增肌|维持|控重|体重管理|恢复|训练表现|运动表现/i,
|
||||
/weight\s*loss|fat\s*loss|muscle\s*gain|maintenance|performance|recovery/i,
|
||||
];
|
||||
|
||||
const matched = [...keywordPatterns, ...structureHints, ...goalHints].some((re) => re.test(text));
|
||||
|
||||
// 若用户发的是极短的承接语,但上下文包含饮食关键词,也认为是营养话题
|
||||
if (!matched && currentText && currentText.length <= 8) {
|
||||
const shortFollowUps = /(那早餐呢|那午餐呢|那晚餐呢|那怎么吃|吃什么|怎么搭配|怎么配|怎么安排|如何吃)/i;
|
||||
if (shortFollowUps.test(currentText)) {
|
||||
const context = (messages || []).slice(-8).map((m) => m.content).join('\n');
|
||||
if ([...keywordPatterns, ...structureHints].some((re) => re.test(context))) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
private deriveTitleIfEmpty(assistantReply: string): string | null {
|
||||
if (!assistantReply) return null;
|
||||
const firstLine = assistantReply.split(/\r?\n/).find(Boolean) || '';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength, IsInt, Min, Max } from 'class-validator';
|
||||
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength, IsInt, Min, Max, IsEnum } from 'class-validator';
|
||||
|
||||
export class AiChatMessageDto {
|
||||
@ApiProperty({ enum: ['user', 'assistant', 'system'] })
|
||||
@@ -39,5 +39,118 @@ export class AiChatResponseDto {
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
// 营养分析相关的DTO
|
||||
export enum NutritionGoal {
|
||||
WEIGHT_LOSS = 'weight_loss',
|
||||
MUSCLE_GAIN = 'muscle_gain',
|
||||
MAINTENANCE = 'maintenance',
|
||||
PERFORMANCE = 'performance',
|
||||
RECOVERY = 'recovery',
|
||||
GENERAL_HEALTH = 'general_health'
|
||||
}
|
||||
|
||||
export class MealItemDto {
|
||||
@ApiProperty({ description: '食物名称' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
foodName: string;
|
||||
|
||||
@ApiProperty({ description: '食物份量(如:1碗、200g、1个等)', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
portion?: string;
|
||||
|
||||
@ApiProperty({ description: '估算热量(卡路里)', required: false })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
estimatedCalories?: number;
|
||||
}
|
||||
|
||||
export class MealDto {
|
||||
@ApiProperty({ description: '餐次名称(早餐/午餐/晚餐/加餐)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
mealType: string;
|
||||
|
||||
@ApiProperty({ description: '用餐时间(如:8:00)', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mealTime?: string;
|
||||
|
||||
@ApiProperty({ type: [MealItemDto], description: '该餐的食物列表' })
|
||||
@IsArray()
|
||||
items: MealItemDto[];
|
||||
}
|
||||
|
||||
export class NutritionAnalysisRequestDto {
|
||||
@ApiProperty({ description: '会话ID。未提供则创建新会话' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
conversationId?: string;
|
||||
|
||||
@ApiProperty({ type: [MealDto], description: '全天的饮食记录' })
|
||||
@IsArray()
|
||||
meals: MealDto[];
|
||||
|
||||
@ApiProperty({ enum: NutritionGoal, description: '营养目标' })
|
||||
@IsEnum(NutritionGoal)
|
||||
goal: NutritionGoal;
|
||||
|
||||
@ApiProperty({ description: '用户当前体重(kg)', required: false })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(20)
|
||||
@Max(300)
|
||||
currentWeight?: number;
|
||||
|
||||
@ApiProperty({ description: '用户身高(cm)', required: false })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(100)
|
||||
@Max(250)
|
||||
height?: number;
|
||||
|
||||
@ApiProperty({ description: '运动强度(1-5级,1最轻,5最重)', required: false })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
activityLevel?: number;
|
||||
|
||||
@ApiProperty({ description: '特殊饮食需求或限制(如:素食、无麸质等)', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dietaryRestrictions?: string;
|
||||
|
||||
@ApiProperty({ description: '过敏食物列表', required: false })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
allergies?: string[];
|
||||
|
||||
@ApiProperty({ description: '是否启用流式输出', default: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
stream?: boolean;
|
||||
}
|
||||
|
||||
export class NutritionAnalysisResponseDto {
|
||||
@ApiProperty()
|
||||
conversationId: string;
|
||||
|
||||
@ApiProperty({ description: '营养分析结果' })
|
||||
analysis: {
|
||||
totalCalories: number;
|
||||
protein: number;
|
||||
carbohydrates: number;
|
||||
fat: number;
|
||||
fiber: number;
|
||||
goalMatchScore: number; // 0-100
|
||||
recommendations: string[];
|
||||
warnings: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user