feat: 儿童心理陪伴筛查插件初始版本
- 核心 PsychoScreener 模块,支持 MiniMax API 调用 - 8 种心理问题类别检测(霸凌、抑郁、焦虑、家庭矛盾等) - ScreeningResult 数据模型,含类别/严重程度/建议 - 单元测试 12 个(含参数化测试,覆盖 7 个虚构场景) - build_response_prefix() 支持检测后注入前缀标记 - pyproject.toml + .gitignore 完整项目脚手架main
commit
d5e64f40c4
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 接入点
|
||||
- [ ] 构建案例库系统
|
||||
- [ ] 开发咨询师终端
|
||||
- [ ] 家长端报告界面
|
||||
|
|
@ -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"]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
儿童心理陪伴 - 核心筛查模块
|
||||
功能:对儿童对话上下文进行心理问题筛查,检测潜在心理需求或问题
|
||||
目标年龄段:3-8岁
|
||||
"""
|
||||
|
||||
from .screener import PsychoScreener, ScreeningResult
|
||||
|
||||
__all__ = ["PsychoScreener", "ScreeningResult"]
|
||||
|
|
@ -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}"
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Test configuration for pytest
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue