commit d5e64f40c4b653462217824785252220c80f83f3 Author: Sileya Date: Sat Apr 4 17:04:04 2026 +0800 feat: 儿童心理陪伴筛查插件初始版本 - 核心 PsychoScreener 模块,支持 MiniMax API 调用 - 8 种心理问题类别检测(霸凌、抑郁、焦虑、家庭矛盾等) - ScreeningResult 数据模型,含类别/严重程度/建议 - 单元测试 12 个(含参数化测试,覆盖 7 个虚构场景) - build_response_prefix() 支持检测后注入前缀标记 - pyproject.toml + .gitignore 完整项目脚手架 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..236c5f0 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# 儿童心理陪伴玩偶 - 环境变量配置 +# 复制此文件为 .env 并填入实际值 + +# MiniMax API Key(必填) +# 获取地址:https://platform.minimaxi.com/ +MINIMAX_API_KEY=your-api-key-here + +# MiniMax 模型(可选,默认 MiniMax-M2.5) +MINIMAX_MODEL=MiniMax-M2.5 + +# MiniMax API 地址(可选,默认使用官方接口) +MINIMAX_BASE_URL=https://api.minimaxi.com/v1/text/chatcompletion_v2 + +# 日志级别 +LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f36b5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.py[cod] +*.so +.venv/ +venv/ +.env + +# Test +.pytest_cache/ +.coverage +htmlcov/ + +# IDE +.idea/ +.vscode/ +*.swp + +# Build +*.egg-info/ +dist/ +build/ + +# OS +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ecca61 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# 儿童心理陪伴玩偶 + +基于小智AI生态的儿童心理筛查插件,通过分析儿童与玩偶的对话内容, +识别潜在的心理问题(如霸凌、抑郁情绪、焦虑、家庭矛盾等), +为家长提供早期预警。 + +## 项目结构 + +``` +child-psycho-companion/ +├── src/ +│ └── psycho_screener/ # 核心筛查模块 +│ ├── __init__.py +│ └── screener.py # 筛查器实现 +├── tests/ +│ ├── __init__.py +│ ├── conftest.py +│ └── test_screener.py # 单元测试 +├── .env.example # 环境变量模板 +├── pyproject.toml +└── README.md +``` + +## 快速开始 + +### 1. 安装 + +```bash +cd child-psycho-companion +pip install -e . +``` + +### 2. 配置 API Key + +```bash +export MINIMAX_API_KEY=your-api-key-here +``` + +### 3. 使用示例 + +```python +from psycho_screener import PsychoScreener + +screener = PsychoScreener(api_key="your-api-key") + +# 对儿童对话进行筛查 +context = """ +孩子:今天在学校,小明又打我了,我好害怕。 +孩子:他说如果我告诉老师就会打我。 +""" +result = screener.screen(context) + +if result.detected: + print(f"检测到问题:{result.summary}") + prefix = screener.build_response_prefix(result) + print(f"响应前缀:{prefix}") +``` + +### 4. 运行测试 + +```bash +# 安装测试依赖 +pip install -e ".[dev]" + +# 运行单元测试(Mock 模式,不调用真实 API) +pytest tests/test_screener.py -v -m unit + +# 运行集成测试(需要真实 API key) +export MINIMAX_API_KEY=your-key +pytest tests/test_screener.py -v -m integration +``` + +## 核心流程 + +``` +儿童语音 → 小智AI (STT) → 对话上下文 + ↓ + 心理筛查器 (MiniMax API) + ↓ + ScreeningResult {detected, category, severity} + ↓ + ┌───────────┴───────────┐ + detected=True detected=False + ↓ ↓ + 注入前缀标记 原样返回 + "已发现特定心理问题:..." +``` + +## 检测类别 + +| 类别 | 描述 | 严重程度 | +|------|------|---------| +| bullying | 霸凌/同伴冲突 | low-high | +| depression | 抑郁情绪 | medium-high | +| anxiety | 焦虑/恐惧 | low-medium | +| family_conflict | 家庭矛盾 | medium-high | +| self_esteem | 自卑/自我否定 | low-medium | +| trauma | 创伤事件 | medium-high | +| social_isolation | 社交孤立 | medium-high | +| other | 其他心理需求 | - | + +## 下一步 + +- [ ] 接入 xinnan-tech/xiaozhi-esp32-server MCP 接入点 +- [ ] 构建案例库系统 +- [ ] 开发咨询师终端 +- [ ] 家长端报告界面 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6e07c4c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "child-psycho-companion" +version = "0.1.0" +description = "儿童心理陪伴玩偶 - 对话心理筛查插件" +requires-python = ">=3.10" +dependencies = [ + "requests>=2.31.0", + "pydantic>=2.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "pytest-mock>=3.14", + "requests-mock>=1.12", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "unit: Unit tests with mocked API calls", + "integration: Integration tests requiring real API calls", +] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/src/psycho_screener/__init__.py b/src/psycho_screener/__init__.py new file mode 100644 index 0000000..60211e0 --- /dev/null +++ b/src/psycho_screener/__init__.py @@ -0,0 +1,9 @@ +""" +儿童心理陪伴 - 核心筛查模块 +功能:对儿童对话上下文进行心理问题筛查,检测潜在心理需求或问题 +目标年龄段:3-8岁 +""" + +from .screener import PsychoScreener, ScreeningResult + +__all__ = ["PsychoScreener", "ScreeningResult"] diff --git a/src/psycho_screener/screener.py b/src/psycho_screener/screener.py new file mode 100644 index 0000000..9f3d149 --- /dev/null +++ b/src/psycho_screener/screener.py @@ -0,0 +1,219 @@ +""" +儿童心理陪伴 - 筛查器 +基于 MiniMax API,对儿童对话进行心理问题筛查 +""" + +from __future__ import annotations + +import os +import json +import requests +from typing import Literal +from pydantic import BaseModel, Field + + +# ============================================================================ +# 数据模型 +# ============================================================================ + +class ConcernCategory(str): + """心理问题类别""" + NONE = "none" + BULLYING = "bullying" # 校园霸凌/同伴冲突 + DEPRESSION = "depression" # 抑郁情绪 + ANXIETY = "anxiety" # 焦虑/恐惧 + FAMILY_CONFLICT = "family_conflict" # 家庭矛盾 + SELF_ESTEEM = "self_esteem" # 自卑/自我否定 + TRAUMA = "trauma" # 创伤事件 + SOCIAL_ISOLATION = "social_isolation" # 社交孤立 + OTHER = "other" # 其他 + + +class ScreeningResult(BaseModel): + """筛查结果""" + detected: bool = Field(description="是否检测到心理问题") + category: str = Field(default=ConcernCategory.NONE, description="问题类别") + severity: Literal["none", "low", "medium", "high"] = Field( + default="none", description="严重程度" + ) + summary: str = Field(default="", description="简要描述检测到的问题") + suggestion: str = Field(default="", description="建议行动") + raw_response: str = Field(default="", description="模型原始响应(调试用)") + + +# ============================================================================ +# 筛查系统提示词 +# ============================================================================ + +SYSTEM_PROMPT = """你是一个专业的儿童心理咨询师助手,专注于分析3-8岁儿童的对话内容,识别潜在的心理需求或问题。 + +## 你的任务 +分析给定的儿童对话上下文,判断是否存在以下心理问题类别: + +1. **bullying** - 霸凌/同伴冲突:孩子表达被欺负、被嘲笑、被孤立、被人威胁等 +2. **depression** - 抑郁情绪:孩子表达悲伤、绝望、无助、对事物失去兴趣、提到"不想活了"等 +3. **anxiety** - 焦虑/恐惧:孩子表达担心、害怕、做噩梦、回避某些情境等 +4. **family_conflict** - 家庭矛盾:孩子表达父母争吵、离婚担心、被忽视、被严厉惩罚等 +5. **self_esteem** - 自卑/自我否定:孩子表达"我不行"、"没人喜欢我"、"我太笨了"等 +6. **trauma** - 创伤事件:孩子描述意外事故、暴力事件、亲人离世等创伤性经历 +7. **social_isolation** - 社交孤立:孩子表达没有朋友、被排斥、孤独感等 +8. **other** - 其他值得关注的心理需求 + +## 输出格式 +请严格按以下JSON格式返回(不要添加任何额外内容): +{ + "detected": true/false, + "category": "具体类别", + "severity": "none/low/medium/high", + "summary": "一句话描述检测到的问题", + "suggestion": "建议的应对方式(简短,1-2句话)" +} + +## 判断标准 +- **low**: 轻微迹象,需要关注但无需立即介入 +- **medium**: 中度迹象,建议与家长沟通 +- **high**: 严重迹象,需要专业干预 + +如果对话内容完全正常,没有任何心理问题迹象,返回: +{ + "detected": false, + "category": "none", + "severity": "none", + "summary": "未检测到心理问题", + "suggestion": "" +} + +注意: +- 只关注确实存在问题的迹象,不要过度解读 +- 儿童的语言表达可能不精确,需要结合上下文判断 +- 正常的情绪表达(偶尔哭、发脾气)不构成问题""" + + +# ============================================================================ +# 筛查器 +# ============================================================================ + +class PsychoScreener: + """ + 儿童心理问题筛查器 + + 使用方法: + screener = PsychoScreener(api_key="your-api-key") + result = screener.screen("今天小明打我了,我很伤心") + if result.detected: + print(f"检测到问题:{result.summary}") + """ + + def __init__( + self, + api_key: str | None = None, + model: str = "MiniMax-M2.5", + base_url: str = "https://api.minimaxi.com/v1/text/chatcompletion_v2", + ): + self.api_key = api_key or os.environ.get("MINIMAX_API_KEY", "") + self.model = model + self.base_url = base_url + + if not self.api_key: + raise ValueError( + "MiniMax API key is required. " + "Set MINIMAX_API_KEY env var or pass api_key parameter." + ) + + def _call_minimax(self, messages: list[dict]) -> str: + """调用 MiniMax API""" + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + payload = { + "model": self.model, + "messages": messages, + } + + response = requests.post( + self.base_url, + headers=headers, + json=payload, + timeout=30, + ) + response.raise_for_status() + data = response.json() + + # 兼容不同返回格式 + if "choices" in data: + return data["choices"][0]["message"]["content"] + elif "output" in data: + return data["output"] + return str(data) + + def screen(self, context: str) -> ScreeningResult: + """ + 对给定的对话上下文进行心理问题筛查 + + Args: + context: 儿童的对话上下文(可以是多轮对话的汇总文本) + + Returns: + ScreeningResult: 包含检测结果的数据模型 + """ + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": f"请分析以下儿童对话内容:\n\n{context}"}, + ] + + try: + raw_response = self._call_minimax(messages) + except Exception as e: + return ScreeningResult( + detected=False, + category=ConcernCategory.OTHER, + severity="none", + summary=f"API调用失败: {str(e)}", + suggestion="", + raw_response=str(e), + ) + + # 尝试解析 JSON + try: + # 提取 JSON(可能模型返回带有 markdown 代码块) + content = raw_response.strip() + if content.startswith("```"): + lines = content.split("\n") + content = "\n".join(lines[1:-1]) # 去掉 ```json 和 ``` + + parsed = json.loads(content) + return ScreeningResult( + detected=parsed.get("detected", False), + category=parsed.get("category", ConcernCategory.NONE), + severity=parsed.get("severity", "none"), + summary=parsed.get("summary", ""), + suggestion=parsed.get("suggestion", ""), + raw_response=raw_response, + ) + except json.JSONDecodeError: + # 无法解析 JSON,返回原始内容 + return ScreeningResult( + detected=False, + category=ConcernCategory.OTHER, + severity="none", + summary="无法解析模型响应", + suggestion="", + raw_response=raw_response, + ) + + def build_response_prefix(self, result: ScreeningResult) -> str: + """ + 根据筛查结果构建响应前缀 + + Args: + result: 筛查结果 + + Returns: + str: 如果检测到问题,返回前缀字符串;否则返回空字符串 + """ + if not result.detected: + return "" + + return f"【已发现特定心理问题】类别:{result.category},严重程度:{result.severity},描述:{result.summary}" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..28cac66 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +# Test configuration for pytest diff --git a/tests/test_screener.py b/tests/test_screener.py new file mode 100644 index 0000000..3cb46c1 --- /dev/null +++ b/tests/test_screener.py @@ -0,0 +1,456 @@ +""" +单元测试 - 儿童心理筛查器 + +测试场景:使用虚构的儿童对话上下文,验证筛查函数能否正确识别并标注心理问题 + +运行方式: + pytest tests/test_screener.py -v +""" + +import pytest +import json +from unittest.mock import patch, MagicMock +from psycho_screener.screener import ( + PsychoScreener, + ScreeningResult, + ConcernCategory, +) + + +# ============================================================================ +# Mock API 响应(用于测试) +# ============================================================================ + +MOCK_RESPONSES = { + # 场景1:霸凌/同伴冲突 + "bullying": json.dumps({ + "detected": True, + "category": "bullying", + "severity": "medium", + "summary": "孩子描述被同学欺负、被嘲笑,感到伤心和无助", + "suggestion": "建议家长与孩子耐心沟通,了解具体情况,并与学校老师联系" + }), + + # 场景2:抑郁情绪 + "depression": json.dumps({ + "detected": True, + "category": "depression", + "severity": "high", + "summary": "孩子表达对生活失去兴趣、经常哭泣、觉得自己没有价值", + "suggestion": "高度建议寻求专业儿童心理咨询师帮助,密切关注孩子的安全" + }), + + # 场景3:焦虑/恐惧 + "anxiety": json.dumps({ + "detected": True, + "category": "anxiety", + "severity": "low", + "summary": "孩子表达对即将到来的考试感到紧张,有些担心", + "suggestion": "可以通过游戏和放松训练帮助孩子缓解焦虑情绪" + }), + + # 场景4:正常对话(无问题) + "normal": json.dumps({ + "detected": False, + "category": "none", + "severity": "none", + "summary": "未检测到心理问题", + "suggestion": "" + }), + + # 场景5:家庭矛盾 + "family_conflict": json.dumps({ + "detected": True, + "category": "family_conflict", + "severity": "high", + "summary": "孩子描述父母经常争吵、提到害怕父母离婚", + "suggestion": "建议家长注意家庭氛围对孩子的影响,考虑家庭咨询" + }), + + # 场景6:自卑/自我否定 + "self_esteem": json.dumps({ + "detected": True, + "category": "self_esteem", + "severity": "medium", + "summary": "孩子反复说自己很笨、什么都不如别人、不值得被喜欢", + "suggestion": "建议多给予孩子正面鼓励和肯定,避免过度批评" + }), + + # 场景7:社交孤立 + "social_isolation": json.dumps({ + "detected": True, + "category": "social_isolation", + "severity": "medium", + "summary": "孩子表达在学校没有朋友、午餐时间独自一人、感到孤独", + "suggestion": "建议帮助孩子学习社交技巧,与学校合作创造更多交友机会" + }), +} + + +# ============================================================================ +# 测试用例:虚构儿童对话上下文 +# ============================================================================ + +TEST_CASES = [ + # ---- 霸凌/同伴冲突 ---- + { + "case_id": "bullying_01", + "scenario": "同伴冲突", + "context": ( + "孩子:今天在学校,小明又打我了,他抢了我的玩具还不让我告诉老师," + "他说如果我告诉老师就会打我。我很害怕,我现在不想去学校了。\n" + "孩子:每次下课我都躲起来,因为我怕再被他欺负。\n" + ), + "expected_detected": True, + "expected_category": "bullying", + "expected_severity_min": "medium", # minimum: at least medium + }, + + # ---- 抑郁情绪 ---- + { + "case_id": "depression_01", + "scenario": "抑郁情绪", + "context": ( + "孩子:最近我总是觉得很累,什么都不想做,连我最喜欢的动画片都不想看了。\n" + "孩子:我觉得我是一个很糟糕的人,没有人喜欢我。\n" + "孩子:有时候我想,要是我不在了就好了。\n" + ), + "expected_detected": True, + "expected_category": "depression", + "expected_severity_min": "high", + }, + + # ---- 焦虑/恐惧 ---- + { + "case_id": "anxiety_01", + "scenario": "分离焦虑", + "context": ( + "孩子:明天妈妈要送我上幼儿园,我好害怕,我不想和妈妈分开。\n" + "孩子:我担心在幼儿园里会做噩梦,害怕一个人睡觉。\n" + ), + "expected_detected": True, + "expected_category": "anxiety", + "expected_severity_min": "low", + }, + + # ---- 正常对话 ---- + { + "case_id": "normal_01", + "scenario": "正常对话", + "context": ( + "孩子:今天我画了一幅画,是一只大恐龙!\n" + "孩子:晚上吃的是我最喜欢的红烧肉,好开心呀!\n" + "孩子:明天我想和好朋友一起去公园玩。\n" + ), + "expected_detected": False, + "expected_category": "none", + "expected_severity_min": "none", + }, + + # ---- 家庭矛盾 ---- + { + "case_id": "family_conflict_01", + "scenario": "家庭矛盾", + "context": ( + "孩子:昨天晚上爸爸妈妈又吵架了,吵得很凶,妈妈哭了。\n" + "孩子:我很害怕,怕他们会离婚。\n" + "孩子:我觉得是我的错,是因为我表现不好他们才吵架的。\n" + ), + "expected_detected": True, + "expected_category": "family_conflict", + "expected_severity_min": "high", + }, + + # ---- 自卑/自我否定 ---- + { + "case_id": "self_esteem_01", + "scenario": "自卑/自我否定", + "context": ( + "孩子:今天老师表扬了小红但是没有表扬我,我就是个笨蛋,什么都做不好。\n" + "孩子:班上的同学都不喜欢我,没有人想和我坐同桌。\n" + "孩子:我什么都学不会,我好笨啊。\n" + ), + "expected_detected": True, + "expected_category": "self_esteem", + "expected_severity_min": "medium", + }, + + # ---- 社交孤立 ---- + { + "case_id": "social_isolation_01", + "scenario": "社交孤立", + "context": ( + "孩子:今天课间的时候我一个人蹲在角落,没有人来和我玩。\n" + "孩子:同学们都有自己的朋友,只有我是一个人。\n" + "孩子:我不想去学校了,那里好孤单。\n" + ), + "expected_detected": True, + "expected_category": "social_isolation", + "expected_severity_min": "medium", + }, +] + + +# ============================================================================ +# Fixtures +# ============================================================================ + +@pytest.fixture +def api_key(): + """测试用 API key(不会被真实调用)""" + return "test-minimax-api-key-12345" + + +@pytest.fixture +def screener(api_key): + """创建筛查器实例""" + return PsychoScreener(api_key=api_key) + + +# ============================================================================ +# 测试:真实调用 MiniMax API(需要有效的 API key) +# ============================================================================ + +@pytest.mark.integration +class TestPsychoScreenerIntegration: + """集成测试:真实调用 MiniMax API(需设置 MINIMAX_API_KEY)""" + + @pytest.fixture(autouse=True) + def check_api_key(self): + api_key = os.environ.get("MINIMAX_API_KEY", "") + if not api_key: + pytest.skip("MINIMAX_API_KEY not set, skipping integration test") + + def test_screen_bullying(self): + api_key = os.environ.get("MINIMAX_API_KEY") + screener = PsychoScreener(api_key=api_key) + + context = TEST_CASES[0]["context"] + result = screener.screen(context) + + print(f"\n[integration] bullying result: {result.model_dump_json(indent=2)}") + assert result.detected is True + assert result.category == "bullying" + + def test_screen_normal(self): + api_key = os.environ.get("MINIMAX_API_KEY") + screener = PsychoScreener(api_key=api_key) + + context = TEST_CASES[3]["context"] + result = screener.screen(context) + + print(f"\n[integration] normal result: {result.model_dump_json(indent=2)}") + assert result.detected is False + + +# ============================================================================ +# 测试:Mock 测试(不真实调用 API) +# ============================================================================ + +@pytest.mark.unit +class TestPsychoScreenerUnit: + """单元测试:使用 Mock API 响应""" + + def _mock_api_call(self, case_id: str) -> MagicMock: + """根据 case_id 返回对应的 mock 响应""" + # 从 case_id 提取场景 key + scenario_map = { + "bullying": "bullying", + "depression": "depression", + "anxiety": "anxiety", + "normal": "normal", + "family_conflict": "family_conflict", + "self_esteem": "self_esteem", + "social_isolation": "social_isolation", + } + + for key, scenario in scenario_map.items(): + if key in case_id: + mock_resp = MagicMock() + mock_resp.raise_for_status = MagicMock() + mock_resp.json.return_value = { + "choices": [{"message": {"content": MOCK_RESPONSES[scenario]}}] + } + return mock_resp + + # 默认返回 normal + mock_resp = MagicMock() + mock_resp.raise_for_status = MagicMock() + mock_resp.json.return_value = { + "choices": [{"message": {"content": MOCK_RESPONSES["normal"]}}] + } + return mock_resp + + @pytest.mark.parametrize("test_case", TEST_CASES, ids=lambda tc: tc["case_id"]) + def test_screen_cases(self, api_key, test_case, requests_mock): + """参数化测试所有场景""" + case_id = test_case["case_id"] + + # Mock HTTP POST + requests_mock.post( + "https://api.minimaxi.com/v1/text/chatcompletion_v2", + json=self._mock_api_call(case_id).json(), + ) + + screener = PsychoScreener(api_key=api_key) + result = screener.screen(test_case["context"]) + + # 打印测试结果(方便调试) + print(f"\n[test] {case_id} ({test_case['scenario']})") + print(f" detected={result.detected}, category={result.category}, severity={result.severity}") + print(f" summary={result.summary}") + + # 验证检测结果 + assert result.detected == test_case["expected_detected"], \ + f"[{case_id}] 检测结果不符:expected {test_case['expected_detected']}, got {result.detected}" + + if test_case["expected_detected"]: + assert result.category == test_case["expected_category"], \ + f"[{case_id}] 类别不符:expected {test_case['expected_category']}, got {result.category}" + + # 验证严重程度(使用顺序比较) + severity_order = ["none", "low", "medium", "high"] + min_idx = severity_order.index(test_case["expected_severity_min"]) + result_idx = severity_order.index(result.severity) + assert result_idx >= min_idx, \ + f"[{case_id}] 严重程度不足:expected >= {test_case['expected_severity_min']}, got {result.severity}" + + def test_build_response_prefix_detected(self, api_key): + """测试:检测到问题时生成前缀""" + screener = PsychoScreener(api_key=api_key) + + result = ScreeningResult( + detected=True, + category="bullying", + severity="medium", + summary="孩子描述被同学欺负", + suggestion="建议与家长沟通", + raw_response="", + ) + + prefix = screener.build_response_prefix(result) + print(f"\nprefix: {prefix}") + + assert "已发现特定心理问题" in prefix + assert "bullying" in prefix + assert "medium" in prefix + + def test_build_response_prefix_not_detected(self, api_key): + """测试:未检测到问题时返回空前缀""" + screener = PsychoScreener(api_key=api_key) + + result = ScreeningResult( + detected=False, + category="none", + severity="none", + summary="未检测到心理问题", + suggestion="", + raw_response="", + ) + + prefix = screener.build_response_prefix(result) + assert prefix == "" + + def test_json_parse_error_handling(self, api_key, requests_mock): + """测试:模型返回非 JSON 时优雅处理""" + requests_mock.post( + "https://api.minimaxi.com/v1/text/chatcompletion_v2", + text="This is not JSON response", + ) + + screener = PsychoScreener(api_key=api_key) + result = screener.screen("some context") + + # 应该优雅降级,不抛出异常 + assert result.detected is False + assert "API调用失败" in result.summary + + +# ============================================================================ +# 测试:数据结构验证 +# ============================================================================ + +@pytest.mark.unit +class TestScreeningResultModel: + """测试 ScreeningResult 数据模型""" + + def test_screening_result_valid(self): + """测试有效数据""" + result = ScreeningResult( + detected=True, + category="bullying", + severity="high", + summary="test", + suggestion="test", + raw_response="test", + ) + assert result.detected is True + assert result.category == "bullying" + + def test_screening_result_defaults(self): + """测试默认值""" + result = ScreeningResult(detected=False) + assert result.detected is False + assert result.category == "none" + assert result.severity == "none" + assert result.summary == "" + + +# ============================================================================ +# 测试:集成小智AI流程 +# ============================================================================ + +@pytest.mark.integration +class TestXiaozhiIntegration: + """ + 测试:与小智AI的集成流程 + + 假设 xiaozhi-esp32-server 返回的对话上下文经过本筛查器, + 验证端到端的处理流程 + """ + + def test_full_flow_with_bullying(self): + """ + 模拟完整流程: + 1. 孩子说了一段可能被霸凌的话 + 2. 筛查器识别出问题 + 3. 原有回复被加上前缀 + """ + # 模拟 xiaozhi 返回的原始回复 + original_response = "不要难过的小朋友,每个人都值得被友好对待哦。" + + # 这里需要真实 API key + api_key = os.environ.get("MINIMAX_API_KEY", "") + if not api_key: + pytest.skip("MINIMAX_API_KEY not set") + + screener = PsychoScreener(api_key=api_key) + + context = ( + "孩子:今天小朋友又欺负我了,把我推倒在地上,还骂我。\n" + "孩子:我觉得好委屈,为什么他们要这样对我?" + ) + + # 步骤1:筛查 + screening_result = screener.screen(context) + print(f"\n[flow] screening result: {screening_result.model_dump_json(indent=2)}") + + # 步骤2:构建前缀 + prefix = screener.build_response_prefix(screening_result) + + # 步骤3:覆盖原有回复 + if prefix: + final_response = f"{prefix}\n\n{original_response}" + else: + final_response = original_response + + print(f"\n[flow] final response:\n{final_response}") + + # 验证前缀存在 + if screening_result.detected: + assert "已发现特定心理问题" in final_response + else: + assert "已发现特定心理问题" not in final_response + + +import os # for integration tests