diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index fcea7a65..d17b998c 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -186,7 +186,7 @@ async def get_emotion_health( "情绪健康指数获取成功", extra={ "end_user_id": request.end_user_id, - "health_score": data.get("health_score", 0), + "health_score": data.get("health_score") or 0, "level": data.get("level", "未知") } ) diff --git a/api/app/controllers/memory_episodic_controller.py b/api/app/controllers/memory_episodic_controller.py index 331adfd3..63f85265 100644 --- a/api/app/controllers/memory_episodic_controller.py +++ b/api/app/controllers/memory_episodic_controller.py @@ -3,9 +3,10 @@ 包含情景记忆总览和详情查询接口 """ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Header from app.core.error_codes import BizCode +from app.core.language_utils import get_language_from_header from app.core.logging_config import get_api_logger from app.core.response_utils import fail, success from app.dependencies import get_current_user @@ -14,6 +15,7 @@ from app.schemas.response_schema import ApiResponse from app.schemas.memory_episodic_schema import ( EpisodicMemoryOverviewRequest, EpisodicMemoryDetailsRequest, + translate_episodic_type, ) from app.services.memory_episodic_service import memory_episodic_service @@ -84,6 +86,7 @@ async def get_episodic_memory_overview_api( @router.post("/details", response_model=ApiResponse) async def get_episodic_memory_details_api( request: EpisodicMemoryDetailsRequest, + language_type: str = Header(default=None, alias="X-Language-Type"), current_user: User = Depends(get_current_user), ) -> dict: """ @@ -111,6 +114,11 @@ async def get_episodic_memory_details_api( summary_id=request.summary_id ) + # 根据语言参数翻译 episodic_type + language = get_language_from_header(language_type) + if "episodic_type" in result: + result["episodic_type"] = translate_episodic_type(result["episodic_type"], language) + api_logger.info( f"成功获取情景记忆详情: end_user_id={request.end_user_id}, summary_id={request.summary_id}" ) diff --git a/api/app/controllers/ontology_controller.py b/api/app/controllers/ontology_controller.py index 9d0511ea..49a2fb3a 100644 --- a/api/app/controllers/ontology_controller.py +++ b/api/app/controllers/ontology_controller.py @@ -233,6 +233,14 @@ async def extract_ontology( language=language ) + # 根据语言类型统一 name 字段 + # zh: name 使用 name_chinese(中文名) + # en: name 保持原值(英文 PascalCase) + if language == "zh": + for cls in result.classes: + if cls.name_chinese: + cls.name = cls.name_chinese + # 构建响应 response = ExtractionResponse( classes=result.classes, diff --git a/api/app/core/logging_config.py b/api/app/core/logging_config.py index 40259b46..28a98a46 100644 --- a/api/app/core/logging_config.py +++ b/api/app/core/logging_config.py @@ -70,8 +70,10 @@ class Neo4jSuccessNotificationFilter(logging.Filter): Returns: True表示允许记录,False表示拒绝(过滤掉) """ - # 只处理 WARNING 级别的日志 - if record.levelno != logging.WARNING: + # 只处理 INFO 和 WARNING 级别的日志 + # Neo4j 驱动对 severity='INFORMATION' 的通知使用 INFO 级别, + # 对 severity='WARNING' 的通知使用 WARNING 级别 + if record.levelno not in (logging.INFO, logging.WARNING): return True # 检查是否是 Neo4j 的成功通知 @@ -110,17 +112,25 @@ class LoggingConfig: root_logger = logging.getLogger() root_logger.setLevel(getattr(logging, settings.LOG_LEVEL.upper())) - # 为 Neo4j 驱动添加过滤器,过滤成功/信息性通知但保留真正的警告 - # Neo4j 驱动会以 WARNING 级别记录所有数据库通知,包括成功(00000)和信息性(00NA0)通知 - # 使用过滤器而不是改变日志级别,这样可以保留真正的警告和错误 - neo4j_filter = Neo4jSuccessNotificationFilter() - for neo4j_logger_name in ["neo4j", "neo4j.io", "neo4j.pool"]: - neo4j_logger = logging.getLogger(neo4j_logger_name) - neo4j_logger.addFilter(neo4j_filter) - # 清除现有处理器 root_logger.handlers.clear() + # Neo4j 通知过滤器 - 挂在 handler 上确保所有传播上来的日志都能被过滤 + neo4j_filter = Neo4jSuccessNotificationFilter() + + # 抑制 Neo4j 通知日志 + # Neo4j 驱动内部会给 neo4j.notifications logger 配置自己的 handler, + # 导致日志绕过根 logger 的 filter 直接输出。 + # 多管齐下确保过滤生效: + # 1. 设置 neo4j.notifications 级别为 WARNING(过滤 INFO 级别的 00NA0 通知) + # 2. 在所有 neo4j logger 上添加 filter(过滤 WARNING 级别的成功通知) + # 3. 在根 handler 上也添加 filter(兜底) + neo4j_notifications_logger = logging.getLogger("neo4j.notifications") + neo4j_notifications_logger.setLevel(logging.WARNING) + for neo4j_logger_name in ["neo4j", "neo4j.io", "neo4j.pool", "neo4j.notifications"]: + neo4j_logger = logging.getLogger(neo4j_logger_name) + neo4j_logger.addFilter(neo4j_filter) + # 创建格式化器 formatter = logging.Formatter( fmt=settings.LOG_FORMAT, @@ -136,6 +146,7 @@ class LoggingConfig: console_handler.setFormatter(formatter) console_handler.setLevel(getattr(logging, settings.LOG_LEVEL.upper())) console_handler.addFilter(sensitive_filter) + console_handler.addFilter(neo4j_filter) root_logger.addHandler(console_handler) # 文件处理器(带轮转) @@ -149,6 +160,7 @@ class LoggingConfig: file_handler.setFormatter(formatter) file_handler.setLevel(getattr(logging, settings.LOG_LEVEL.upper())) file_handler.addFilter(sensitive_filter) + file_handler.addFilter(neo4j_filter) root_logger.addHandler(file_handler) cls._initialized = True diff --git a/api/app/core/memory/utils/prompt/prompt_utils.py b/api/app/core/memory/utils/prompt/prompt_utils.py index 06264693..50d31f2a 100644 --- a/api/app/core/memory/utils/prompt/prompt_utils.py +++ b/api/app/core/memory/utils/prompt/prompt_utils.py @@ -349,19 +349,39 @@ async def render_emotion_suggestions_prompt( import json # 预处理 emotion_distribution 为 JSON 字符串 + # 如果是中文,将 emotion_distribution 的 key 翻译为中文 + emotion_distribution = health_data.get('emotion_distribution', {}) + if language == "zh": + emotion_type_zh = { + 'joy': '喜悦', 'sadness': '悲伤', 'anger': '愤怒', + 'fear': '恐惧', 'surprise': '惊讶', 'neutral': '中性' + } + emotion_distribution = { + emotion_type_zh.get(k, k): v for k, v in emotion_distribution.items() + } emotion_distribution_json = json.dumps( - health_data.get('emotion_distribution', {}), + emotion_distribution, ensure_ascii=False, indent=2 ) + # 翻译 dominant_negative_emotion + dominant_negative_translated = None + dominant_neg = patterns.get('dominant_negative_emotion') + if dominant_neg and language == "zh": + emotion_type_zh_map = { + 'sadness': '悲伤', 'anger': '愤怒', 'fear': '恐惧' + } + dominant_negative_translated = emotion_type_zh_map.get(dominant_neg, dominant_neg) + template = prompt_env.get_template("generate_emotion_suggestions.jinja2") rendered_prompt = template.render( health_data=health_data, patterns=patterns, user_profile=user_profile, emotion_distribution_json=emotion_distribution_json, - language=language + language=language, + dominant_negative_translated=dominant_negative_translated ) # 记录渲染结果到提示日志 diff --git a/api/app/core/memory/utils/prompt/prompts/generate_emotion_suggestions.jinja2 b/api/app/core/memory/utils/prompt/prompts/generate_emotion_suggestions.jinja2 index 23b1a583..d4c9d79a 100644 --- a/api/app/core/memory/utils/prompt/prompts/generate_emotion_suggestions.jinja2 +++ b/api/app/core/memory/utils/prompt/prompts/generate_emotion_suggestions.jinja2 @@ -1,10 +1,23 @@ {% if language == "en" %} You are a professional mental health consultant. Based on the following user's emotional health data and personal information, generate 3-5 personalized emotional improvement suggestions. +## Core Principle (Highest Priority) + +**You must strictly base your suggestions on the emotion distribution data provided below. As long as any emotion type has a count ≥ 1, that emotion EXISTS and you must acknowledge and address it in your suggestions. You must NEVER claim an emotion is "zero" or "absent" when its count is ≥ 1.** + +Specific rules: +1. Carefully check the count for each emotion type in "Emotion Distribution" — count ≥ 1 means the emotion exists +2. Even if an emotion appeared only once, you must mention it in health_summary or suggestions and provide targeted advice +3. Never state that an emotion is "zero" or "non-existent" unless its count in the distribution data is truly 0 +4. If positive emotions (e.g., Joy) exist, health_summary must affirm this positive signal +5. If negative emotions (e.g., Sadness, Anger, Fear) exist even once, you must provide targeted improvement suggestions +6. A high proportion of neutral emotions does NOT mean other emotions are absent — address all non-zero emotions + ## User Emotional Health Data Health Score: {{ health_data.health_score }}/100 Health Level: {{ health_data.level }} +Total Emotion Records: {{ health_data.dimensions.positivity_rate.positive_count + health_data.dimensions.positivity_rate.negative_count + health_data.dimensions.positivity_rate.neutral_count }} Dimension Analysis: - Positivity Rate: {{ health_data.dimensions.positivity_rate.score }}/100 @@ -18,7 +31,7 @@ Dimension Analysis: - Resilience: {{ health_data.dimensions.resilience.score }}/100 - Recovery Rate: {{ health_data.dimensions.resilience.recovery_rate }} -Emotion Distribution: +Emotion Distribution (check each item — every emotion with count ≥ 1 must be reflected in suggestions): {{ emotion_distribution_json }} ## Emotion Pattern Analysis @@ -41,6 +54,7 @@ Please generate 3-5 personalized suggestions, each containing: 5. actionable_steps: 3 specific executable steps Also provide a health_summary (no more than 50 words) summarizing the user's overall emotional state. +**The health_summary must truthfully reflect ALL non-zero emotions from the distribution data. Do not omit any emotion type that has appeared.** Please return in JSON format as follows: { @@ -57,6 +71,7 @@ Please return in JSON format as follows: } Notes: +- CRITICAL: Any emotion with count ≥ 1 in the distribution MUST be acknowledged and addressed — never ignore or claim it is zero - Suggestions should be specific and actionable, avoid vague advice - Provide personalized suggestions based on user's interests and hobbies - Provide targeted suggestions for main issues (such as dominant negative emotions) @@ -66,10 +81,23 @@ Notes: {% else %} 你是一位专业的心理健康顾问。请根据以下用户的情绪健康数据和个人信息,生成3-5条个性化的情绪改善建议。 +## 核心原则(最高优先级) + +**你必须严格基于下方提供的情绪分布数据来生成建议。只要某种情绪的出现次数 ≥ 1,就代表该情绪确实存在,你必须在建议中承认并回应这一情绪,绝对不能说"该情绪为零"或"没有该情绪"。** + +具体规则: +1. 仔细查看"情绪分布"中每种情绪的出现次数,次数 ≥ 1 即表示该情绪存在 +2. 即使某种情绪只出现了1次,也必须在 health_summary 或建议中提及并给出针对性建议 +3. 严禁在输出中声称某种情绪"为零"或"不存在",除非该情绪在分布数据中确实为0次 +4. 如果正面情绪(如喜悦)存在,health_summary 中必须肯定这一积极信号 +5. 如果负面情绪(如悲伤、愤怒、恐惧)存在,即使只有1次,也必须给出针对性的改善建议 +6. 中性情绪占比高不代表没有其他情绪,必须同时关注所有非零情绪 + ## 用户情绪健康数据 健康分数:{{ health_data.health_score }}/100 健康等级:{{ health_data.level }} +情绪记录总数:{{ health_data.dimensions.positivity_rate.positive_count + health_data.dimensions.positivity_rate.negative_count + health_data.dimensions.positivity_rate.neutral_count }}条 维度分析: - 积极率:{{ health_data.dimensions.positivity_rate.score }}/100 @@ -83,12 +111,12 @@ Notes: - 恢复力:{{ health_data.dimensions.resilience.score }}/100 - 恢复率:{{ health_data.dimensions.resilience.recovery_rate }} -情绪分布: +情绪分布(请逐项检查,次数≥1的情绪都必须在建议中体现): {{ emotion_distribution_json }} ## 情绪模式分析 -主要负面情绪:{{ patterns.dominant_negative_emotion|default('无') }} +主要负面情绪:{{ dominant_negative_translated|default(patterns.dominant_negative_emotion)|default('无') }} 情绪波动性:{{ patterns.emotion_volatility|default('未知') }} 高强度情绪次数:{{ patterns.high_intensity_emotions|default([])|length }} @@ -106,6 +134,7 @@ Notes: 5. actionable_steps: 3个可执行的具体步骤 同时提供一个health_summary(不超过50字),概括用户的整体情绪状态。 +**health_summary 必须如实反映情绪分布中所有非零情绪的存在,不得遗漏任何已出现的情绪类型。** 请以JSON格式返回,格式如下: { @@ -122,9 +151,11 @@ Notes: } 注意事项: +- 所有输出内容必须完全使用中文,严禁出现任何英文单词或短语(包括情绪类型名称如fear、sadness、anger等,必须使用对应的中文:恐惧、悲伤、愤怒等) +- 再次强调:情绪分布中出现次数≥1的情绪必须在建议中被提及和回应,绝不能忽略或声称为零 - 建议要具体、可执行,避免空泛 - 结合用户的兴趣爱好提供个性化建议 - 针对主要问题(如主要负面情绪)提供针对性建议 -- 优先级要合理分配(至少1个high,1-2个medium,其余low) +- 优先级要合理分配(至少1个高,1-2个中,其余低) - 每个建议的3个步骤要循序渐进、易于实施 {% endif %} diff --git a/api/app/schemas/memory_episodic_schema.py b/api/app/schemas/memory_episodic_schema.py index 74e68837..cb3a874c 100644 --- a/api/app/schemas/memory_episodic_schema.py +++ b/api/app/schemas/memory_episodic_schema.py @@ -25,6 +25,31 @@ type_mapping = { "Condition": "条件实体节点", "Numeric": "数值实体节点" } +EPISODIC_TYPE_MAPPING = { + "conversation": "对话", + "project_work": "项目/工作", + "learning": "学习", + "decision": "决策", + "important_event": "重要事件", +} + + +def translate_episodic_type(episodic_type: str, language: str = "zh") -> str: + """ + 根据语言参数翻译情景类型 + + Args: + episodic_type: 英文枚举值 (conversation, project_work, etc.) + language: 语言类型 ("zh" 中文, "en" 英文) + + Returns: + 翻译后的类型字符串 + """ + if language == "en": + return episodic_type + return EPISODIC_TYPE_MAPPING.get(episodic_type, episodic_type) + + class EmotionType(ABC): JOY_TYPE = "joy" SURPRISE_TYPE = "surprise" @@ -51,7 +76,6 @@ class EpisodicMemoryOverviewRequest(BaseModel): """情景记忆总览查询请求""" end_user_id: str = Field(..., description="终端用户ID") - language_type: Optional[str] = Field("zh", description="语言类型(zh/en)") time_range: str = Field( default="all", description="时间范围筛选,可选值:all, today, this_week, this_month" @@ -71,4 +95,3 @@ class EpisodicMemoryDetailsRequest(BaseModel): end_user_id: str = Field(..., description="终端用户ID") summary_id: str = Field(..., description="情景记忆摘要ID") - language_type: Optional[str] = Field("zh", description="语言类型(zh/en)") diff --git a/api/app/services/emotion_analytics_service.py b/api/app/services/emotion_analytics_service.py index f171aff9..9d60d6bb 100644 --- a/api/app/services/emotion_analytics_service.py +++ b/api/app/services/emotion_analytics_service.py @@ -220,14 +220,16 @@ class EmotionAnalyticsService: """计算积极率 根据情绪类型分类正面、负面和中性情绪,计算积极率。 - 公式:(正面数 / (正面数 + 负面数)) * 100 + 当存在非中性情绪时:(正面数 / (正面数 + 负面数)) * 100 + 当只有中性情绪时:基于中性情绪的存在给出基准分数 + 当完全没有情绪数据时:score 为 None,表示无法计算 Args: emotions: 情绪数据列表,每个包含 emotion_type 字段 Returns: Dict: 包含积极率计算结果: - - score: 积极率分数(0-100) + - score: 积极率分数(0-100),无数据时为 None - positive_count: 正面情绪数量 - negative_count: 负面情绪数量 - neutral_count: 中性情绪数量 @@ -245,14 +247,19 @@ class EmotionAnalyticsService: total_non_neutral = positive_count + negative_count if total_non_neutral > 0: score = (positive_count / total_non_neutral) * 100 + elif neutral_count > 0: + # 只有中性情绪,说明情绪状态平稳,给予基准分 50 + score = 50.0 else: - score = 50.0 # 如果没有非中性情绪,默认为50 + # 完全没有情绪数据,无法计算积极率 + score = None + score_display = f"{score:.2f}" if score is not None else "N/A" logger.debug(f"积极率计算: positive={positive_count}, negative={negative_count}, " - f"neutral={neutral_count}, score={score:.2f}") + f"neutral={neutral_count}, score={score_display}") return { - "score": round(score, 2), + "score": round(score, 2) if score is not None else None, "positive_count": positive_count, "negative_count": negative_count, "neutral_count": neutral_count @@ -385,12 +392,12 @@ class EmotionAnalyticsService: if not emotions: logger.warning(f"用户 {end_user_id} 在时间范围 {time_range} 内没有情绪数据") return { - "health_score": 0.0, + "health_score": None, "level": "无数据", "dimensions": { - "positivity_rate": {"score": 0.0, "positive_count": 0, "negative_count": 0, "neutral_count": 0}, - "stability": {"score": 0.0, "std_deviation": 0.0}, - "resilience": {"score": 0.0, "recovery_rate": 0.0} + "positivity_rate": {"score": None, "positive_count": 0, "negative_count": 0, "neutral_count": 0}, + "stability": {"score": None, "std_deviation": 0.0}, + "resilience": {"score": None, "recovery_rate": 0.0} }, "emotion_distribution": {}, "time_range": time_range @@ -403,8 +410,10 @@ class EmotionAnalyticsService: # 计算综合健康分数 # 公式:positivity_rate * 0.4 + stability * 0.3 + resilience * 0.3 + # 如果积极率无法计算(无数据),视为 0 参与加权 + positivity_score = positivity_rate["score"] if positivity_rate["score"] is not None else 0.0 health_score = ( - positivity_rate["score"] * 0.4 + + positivity_score * 0.4 + stability["score"] * 0.3 + resilience["score"] * 0.3 ) @@ -700,7 +709,7 @@ class EmotionAnalyticsService: Returns: EmotionSuggestionsResponse: 默认建议 """ - health_score = health_data.get('health_score', 0) + health_score = health_data.get('health_score') or 0 if language == "en": if health_score >= 80: