Merge remote-tracking branch 'origin/develop' into refactor/memory-config-management
This commit is contained in:
@@ -14,6 +14,7 @@ from app.core.error_codes import BizCode
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.repositories import workspace_repository, knowledge_repository
|
||||
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
|
||||
@@ -328,4 +329,4 @@ def create_agent_invocation_tool(
|
||||
)
|
||||
return f"调用 Agent 失败: {str(e)}"
|
||||
|
||||
return invoke_agent
|
||||
return invoke_agent
|
||||
@@ -143,7 +143,7 @@ class ApiKeyService:
|
||||
existing = db.scalar(
|
||||
select(ApiKey).where(
|
||||
ApiKey.workspace_id == workspace_id,
|
||||
ApiKey.resource_id == data.resource_id,
|
||||
ApiKey.resource_id == api_key.resource_id,
|
||||
ApiKey.name == data.name,
|
||||
ApiKey.is_active,
|
||||
ApiKey.id != api_key_id
|
||||
@@ -257,7 +257,7 @@ class RateLimiterService:
|
||||
key = f"rate_limit:qps:{api_key_id}"
|
||||
async with self.redis.pipeline() as pipe:
|
||||
pipe.incr(key)
|
||||
pipe.expire(key, 1) # 1 秒过期
|
||||
pipe.expire(key, 1, nx=True) # 1 秒过期
|
||||
results = await pipe.execute()
|
||||
|
||||
current = results[0]
|
||||
|
||||
670
api/app/services/emotion_analytics_service.py
Normal file
670
api/app/services/emotion_analytics_service.py
Normal file
@@ -0,0 +1,670 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""情绪分析服务模块
|
||||
|
||||
本模块提供情绪数据的分析和统计功能,包括情绪标签、词云、健康指数计算等。
|
||||
|
||||
Classes:
|
||||
EmotionAnalyticsService: 情绪分析服务,提供各种情绪分析功能
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
import statistics
|
||||
import json
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.repositories.neo4j.emotion_repository import EmotionRepository
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from app.core.logging_config import get_business_logger
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
|
||||
class EmotionSuggestion(BaseModel):
|
||||
"""情绪建议模型"""
|
||||
type: str = Field(..., description="建议类型:emotion_balance/activity_recommendation/social_connection/stress_management")
|
||||
title: str = Field(..., description="建议标题")
|
||||
content: str = Field(..., description="建议内容")
|
||||
priority: str = Field(..., description="优先级:high/medium/low")
|
||||
actionable_steps: List[str] = Field(..., description="可执行步骤列表(3个)")
|
||||
|
||||
|
||||
class EmotionSuggestionsResponse(BaseModel):
|
||||
"""情绪建议响应模型"""
|
||||
health_summary: str = Field(..., description="健康状态摘要(不超过50字)")
|
||||
suggestions: List[EmotionSuggestion] = Field(..., description="建议列表(3-5条)")
|
||||
|
||||
|
||||
class EmotionAnalyticsService:
|
||||
"""情绪分析服务
|
||||
|
||||
提供情绪数据的分析和统计功能,包括:
|
||||
- 情绪标签统计
|
||||
- 情绪词云数据
|
||||
- 情绪健康指数计算
|
||||
- 个性化情绪建议生成
|
||||
|
||||
Attributes:
|
||||
emotion_repo: 情绪数据仓储实例
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化情绪分析服务"""
|
||||
connector = Neo4jConnector()
|
||||
self.emotion_repo = EmotionRepository(connector)
|
||||
logger.info("情绪分析服务初始化完成")
|
||||
|
||||
async def get_emotion_tags(
|
||||
self,
|
||||
end_user_id: str,
|
||||
emotion_type: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
limit: int = 10
|
||||
) -> Dict[str, Any]:
|
||||
"""获取情绪标签统计
|
||||
|
||||
查询指定用户的情绪类型分布,包括计数、百分比和平均强度。
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
emotion_type: 可选的情绪类型过滤
|
||||
start_date: 可选的开始日期(ISO格式)
|
||||
end_date: 可选的结束日期(ISO格式)
|
||||
limit: 返回结果的最大数量
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪标签统计的响应数据:
|
||||
- tags: 情绪标签列表
|
||||
- total_count: 总情绪数量
|
||||
- time_range: 时间范围信息
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取情绪标签统计: user={end_user_id}, type={emotion_type}, "
|
||||
f"start={start_date}, end={end_date}, limit={limit}")
|
||||
|
||||
# 调用仓储层查询
|
||||
tags = await self.emotion_repo.get_emotion_tags(
|
||||
group_id=end_user_id,
|
||||
emotion_type=emotion_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# 计算总数
|
||||
total_count = sum(tag["count"] for tag in tags)
|
||||
|
||||
# 构建时间范围信息
|
||||
time_range = {}
|
||||
if start_date:
|
||||
time_range["start_date"] = start_date
|
||||
if end_date:
|
||||
time_range["end_date"] = end_date
|
||||
|
||||
# 格式化响应
|
||||
response = {
|
||||
"tags": tags,
|
||||
"total_count": total_count,
|
||||
"time_range": time_range if time_range else None
|
||||
}
|
||||
|
||||
logger.info(f"情绪标签统计完成: total_count={total_count}, tags_count={len(tags)}")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取情绪标签统计失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def get_emotion_wordcloud(
|
||||
self,
|
||||
end_user_id: str,
|
||||
emotion_type: Optional[str] = None,
|
||||
limit: int = 50
|
||||
) -> Dict[str, Any]:
|
||||
"""获取情绪词云数据
|
||||
|
||||
查询情绪关键词及其频率,用于生成词云可视化。
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
emotion_type: 可选的情绪类型过滤
|
||||
limit: 返回关键词的最大数量
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪词云数据的响应:
|
||||
- keywords: 关键词列表
|
||||
- total_keywords: 总关键词数量
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取情绪词云数据: user={end_user_id}, type={emotion_type}, limit={limit}")
|
||||
|
||||
# 调用仓储层查询
|
||||
keywords = await self.emotion_repo.get_emotion_wordcloud(
|
||||
group_id=end_user_id,
|
||||
emotion_type=emotion_type,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# 计算总关键词数量
|
||||
total_keywords = len(keywords)
|
||||
|
||||
# 格式化响应
|
||||
response = {
|
||||
"keywords": keywords,
|
||||
"total_keywords": total_keywords
|
||||
}
|
||||
|
||||
logger.info(f"情绪词云数据获取完成: total_keywords={total_keywords}")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取情绪词云数据失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _calculate_positivity_rate(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""计算积极率
|
||||
|
||||
根据情绪类型分类正面、负面和中性情绪,计算积极率。
|
||||
公式:(正面数 / (正面数 + 负面数)) * 100
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_type 字段
|
||||
|
||||
Returns:
|
||||
Dict: 包含积极率计算结果:
|
||||
- score: 积极率分数(0-100)
|
||||
- positive_count: 正面情绪数量
|
||||
- negative_count: 负面情绪数量
|
||||
- neutral_count: 中性情绪数量
|
||||
"""
|
||||
# 定义情绪分类
|
||||
positive_emotions = {'joy', 'surprise'}
|
||||
negative_emotions = {'sadness', 'anger', 'fear'}
|
||||
|
||||
# 统计各类情绪数量
|
||||
positive_count = sum(1 for e in emotions if e.get('emotion_type') in positive_emotions)
|
||||
negative_count = sum(1 for e in emotions if e.get('emotion_type') in negative_emotions)
|
||||
neutral_count = sum(1 for e in emotions if e.get('emotion_type') == 'neutral')
|
||||
|
||||
# 计算积极率
|
||||
total_non_neutral = positive_count + negative_count
|
||||
if total_non_neutral > 0:
|
||||
score = (positive_count / total_non_neutral) * 100
|
||||
else:
|
||||
score = 50.0 # 如果没有非中性情绪,默认为50
|
||||
|
||||
logger.debug(f"积极率计算: positive={positive_count}, negative={negative_count}, "
|
||||
f"neutral={neutral_count}, score={score:.2f}")
|
||||
|
||||
return {
|
||||
"score": round(score, 2),
|
||||
"positive_count": positive_count,
|
||||
"negative_count": negative_count,
|
||||
"neutral_count": neutral_count
|
||||
}
|
||||
|
||||
def _calculate_stability(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""计算稳定性
|
||||
|
||||
基于情绪强度的标准差计算情绪稳定性。
|
||||
公式:(1 - min(std_deviation, 1.0)) * 100
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_intensity 字段
|
||||
|
||||
Returns:
|
||||
Dict: 包含稳定性计算结果:
|
||||
- score: 稳定性分数(0-100)
|
||||
- std_deviation: 标准差
|
||||
"""
|
||||
# 提取所有情绪强度
|
||||
intensities = [e.get('emotion_intensity', 0.0) for e in emotions if e.get('emotion_intensity') is not None]
|
||||
|
||||
# 计算标准差
|
||||
if len(intensities) >= 2:
|
||||
std_deviation = statistics.stdev(intensities)
|
||||
elif len(intensities) == 1:
|
||||
std_deviation = 0.0 # 只有一个数据点,标准差为0
|
||||
else:
|
||||
std_deviation = 0.0 # 没有数据,标准差为0
|
||||
|
||||
# 计算稳定性分数
|
||||
# 标准差越小,稳定性越高
|
||||
score = (1 - min(std_deviation, 1.0)) * 100
|
||||
|
||||
logger.debug(f"稳定性计算: intensities_count={len(intensities)}, "
|
||||
f"std_deviation={std_deviation:.3f}, score={score:.2f}")
|
||||
|
||||
return {
|
||||
"score": round(score, 2),
|
||||
"std_deviation": round(std_deviation, 3)
|
||||
}
|
||||
|
||||
def _calculate_resilience(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""计算恢复力
|
||||
|
||||
分析情绪转换模式,统计从负面情绪恢复到正面情绪的能力。
|
||||
公式:(负面到正面转换次数 / 总负面情绪数) * 100
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_type 和 created_at 字段
|
||||
应该按时间顺序排列
|
||||
|
||||
Returns:
|
||||
Dict: 包含恢复力计算结果:
|
||||
- score: 恢复力分数(0-100)
|
||||
- recovery_rate: 恢复率(转换次数/负面情绪数)
|
||||
"""
|
||||
# 定义情绪分类
|
||||
positive_emotions = {'joy', 'surprise'}
|
||||
negative_emotions = {'sadness', 'anger', 'fear'}
|
||||
|
||||
# 统计负面到正面的转换次数
|
||||
recovery_count = 0
|
||||
negative_count = 0
|
||||
|
||||
for i in range(len(emotions)):
|
||||
current_emotion = emotions[i].get('emotion_type')
|
||||
|
||||
# 统计负面情绪总数
|
||||
if current_emotion in negative_emotions:
|
||||
negative_count += 1
|
||||
|
||||
# 检查下一个情绪是否为正面
|
||||
if i + 1 < len(emotions):
|
||||
next_emotion = emotions[i + 1].get('emotion_type')
|
||||
if next_emotion in positive_emotions:
|
||||
recovery_count += 1
|
||||
|
||||
# 计算恢复力分数
|
||||
if negative_count > 0:
|
||||
recovery_rate = recovery_count / negative_count
|
||||
score = recovery_rate * 100
|
||||
else:
|
||||
# 如果没有负面情绪,恢复力设为100(最佳状态)
|
||||
recovery_rate = 1.0
|
||||
score = 100.0
|
||||
|
||||
logger.debug(f"恢复力计算: negative_count={negative_count}, "
|
||||
f"recovery_count={recovery_count}, score={score:.2f}")
|
||||
|
||||
return {
|
||||
"score": round(score, 2),
|
||||
"recovery_rate": round(recovery_rate, 3)
|
||||
}
|
||||
|
||||
async def calculate_emotion_health_index(
|
||||
self,
|
||||
end_user_id: str,
|
||||
time_range: str = "30d"
|
||||
) -> Dict[str, Any]:
|
||||
"""计算情绪健康指数
|
||||
|
||||
综合积极率、稳定性和恢复力计算情绪健康指数。
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
time_range: 时间范围(7d/30d/90d)
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪健康指数的完整响应:
|
||||
- health_score: 综合健康分数(0-100)
|
||||
- level: 健康等级(优秀/良好/一般/较差)
|
||||
- dimensions: 各维度详细数据
|
||||
- positivity_rate: 积极率
|
||||
- stability: 稳定性
|
||||
- resilience: 恢复力
|
||||
- emotion_distribution: 情绪分布统计
|
||||
- time_range: 时间范围
|
||||
"""
|
||||
try:
|
||||
logger.info(f"计算情绪健康指数: user={end_user_id}, time_range={time_range}")
|
||||
|
||||
# 获取时间范围内的情绪数据
|
||||
emotions = await self.emotion_repo.get_emotions_in_range(
|
||||
group_id=end_user_id,
|
||||
time_range=time_range
|
||||
)
|
||||
|
||||
# 如果没有数据,返回默认值
|
||||
if not emotions:
|
||||
logger.warning(f"用户 {end_user_id} 在时间范围 {time_range} 内没有情绪数据")
|
||||
return {
|
||||
"health_score": 0.0,
|
||||
"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}
|
||||
},
|
||||
"emotion_distribution": {},
|
||||
"time_range": time_range
|
||||
}
|
||||
|
||||
# 计算各维度指标
|
||||
positivity_rate = self._calculate_positivity_rate(emotions)
|
||||
stability = self._calculate_stability(emotions)
|
||||
resilience = self._calculate_resilience(emotions)
|
||||
|
||||
# 计算综合健康分数
|
||||
# 公式:positivity_rate * 0.4 + stability * 0.3 + resilience * 0.3
|
||||
health_score = (
|
||||
positivity_rate["score"] * 0.4 +
|
||||
stability["score"] * 0.3 +
|
||||
resilience["score"] * 0.3
|
||||
)
|
||||
|
||||
# 确定健康等级
|
||||
if health_score >= 80:
|
||||
level = "优秀"
|
||||
elif health_score >= 60:
|
||||
level = "良好"
|
||||
elif health_score >= 40:
|
||||
level = "一般"
|
||||
else:
|
||||
level = "较差"
|
||||
|
||||
# 统计情绪分布
|
||||
emotion_distribution = {}
|
||||
for emotion_type in ['joy', 'sadness', 'anger', 'fear', 'surprise', 'neutral']:
|
||||
count = sum(1 for e in emotions if e.get('emotion_type') == emotion_type)
|
||||
emotion_distribution[emotion_type] = count
|
||||
|
||||
# 格式化响应
|
||||
response = {
|
||||
"health_score": round(health_score, 2),
|
||||
"level": level,
|
||||
"dimensions": {
|
||||
"positivity_rate": positivity_rate,
|
||||
"stability": stability,
|
||||
"resilience": resilience
|
||||
},
|
||||
"emotion_distribution": emotion_distribution,
|
||||
"time_range": time_range
|
||||
}
|
||||
|
||||
logger.info(f"情绪健康指数计算完成: score={health_score:.2f}, level={level}")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"计算情绪健康指数失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _analyze_emotion_patterns(self, emotions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""分析情绪模式
|
||||
|
||||
识别主要负面情绪、情绪触发因素和波动时段。
|
||||
|
||||
Args:
|
||||
emotions: 情绪数据列表,每个包含 emotion_type、emotion_intensity、created_at 字段
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪模式分析结果:
|
||||
- dominant_negative_emotion: 主要负面情绪类型
|
||||
- high_intensity_emotions: 高强度情绪列表
|
||||
- emotion_volatility: 情绪波动性(高/中/低)
|
||||
"""
|
||||
negative_emotions = {'sadness', 'anger', 'fear'}
|
||||
|
||||
# 统计负面情绪分布
|
||||
negative_emotion_counts = {}
|
||||
for emotion in emotions:
|
||||
emotion_type = emotion.get('emotion_type')
|
||||
if emotion_type in negative_emotions:
|
||||
negative_emotion_counts[emotion_type] = negative_emotion_counts.get(emotion_type, 0) + 1
|
||||
|
||||
# 识别主要负面情绪
|
||||
dominant_negative_emotion = None
|
||||
if negative_emotion_counts:
|
||||
dominant_negative_emotion = max(negative_emotion_counts, key=negative_emotion_counts.get)
|
||||
|
||||
# 识别高强度情绪(强度 >= 0.7)
|
||||
high_intensity_emotions = [
|
||||
{
|
||||
"type": e.get('emotion_type'),
|
||||
"intensity": e.get('emotion_intensity'),
|
||||
"created_at": e.get('created_at')
|
||||
}
|
||||
for e in emotions
|
||||
if e.get('emotion_intensity', 0) >= 0.7
|
||||
]
|
||||
|
||||
# 评估情绪波动性
|
||||
intensities = [e.get('emotion_intensity', 0.0) for e in emotions if e.get('emotion_intensity') is not None]
|
||||
if len(intensities) >= 2:
|
||||
std_dev = statistics.stdev(intensities)
|
||||
if std_dev > 0.3:
|
||||
volatility = "高"
|
||||
elif std_dev > 0.15:
|
||||
volatility = "中"
|
||||
else:
|
||||
volatility = "低"
|
||||
else:
|
||||
volatility = "未知"
|
||||
|
||||
logger.debug(f"情绪模式分析: dominant_negative={dominant_negative_emotion}, "
|
||||
f"high_intensity_count={len(high_intensity_emotions)}, volatility={volatility}")
|
||||
|
||||
return {
|
||||
"dominant_negative_emotion": dominant_negative_emotion,
|
||||
"high_intensity_emotions": high_intensity_emotions[:5], # 最多返回5个
|
||||
"emotion_volatility": volatility
|
||||
}
|
||||
|
||||
async def generate_emotion_suggestions(
|
||||
self,
|
||||
end_user_id: str,
|
||||
config_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""生成个性化情绪建议
|
||||
|
||||
基于情绪健康数据和用户画像生成个性化建议。
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
config_id: 配置ID(可选,用于从数据库加载LLM配置)
|
||||
|
||||
Returns:
|
||||
Dict: 包含个性化建议的响应:
|
||||
- health_summary: 健康状态摘要
|
||||
- suggestions: 建议列表(3-5条)
|
||||
"""
|
||||
try:
|
||||
logger.info(f"生成个性化情绪建议: user={end_user_id}, config_id={config_id}")
|
||||
|
||||
# 1. 如果提供了 config_id,从数据库加载配置
|
||||
if config_id is not None:
|
||||
from app.core.memory.utils.config.definitions import reload_configuration_from_database
|
||||
config_loaded = reload_configuration_from_database(config_id)
|
||||
if not config_loaded:
|
||||
logger.warning(f"无法加载配置 config_id={config_id},将使用默认配置")
|
||||
|
||||
# 2. 获取情绪健康数据
|
||||
health_data = await self.calculate_emotion_health_index(end_user_id, time_range="30d")
|
||||
|
||||
# 3. 获取情绪数据用于模式分析
|
||||
emotions = await self.emotion_repo.get_emotions_in_range(
|
||||
group_id=end_user_id,
|
||||
time_range="30d"
|
||||
)
|
||||
|
||||
# 4. 分析情绪模式
|
||||
patterns = self._analyze_emotion_patterns(emotions)
|
||||
|
||||
# 5. 获取用户画像数据(简化版,直接从Neo4j获取)
|
||||
user_profile = await self._get_simple_user_profile(end_user_id)
|
||||
|
||||
# 6. 构建LLM prompt
|
||||
prompt = await self._build_suggestion_prompt(health_data, patterns, user_profile)
|
||||
|
||||
# 7. 调用LLM生成建议(使用配置中的LLM)
|
||||
from app.core.memory.utils.llm.llm_utils import get_llm_client
|
||||
llm_client = get_llm_client()
|
||||
|
||||
# 将 prompt 转换为 messages 格式
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response = await llm_client.chat(messages=messages)
|
||||
response_text = response.content.strip()
|
||||
|
||||
# 8. 解析LLM响应
|
||||
try:
|
||||
response_data = json.loads(response_text)
|
||||
suggestions_response = EmotionSuggestionsResponse(**response_data)
|
||||
except (json.JSONDecodeError, Exception) as e:
|
||||
logger.error(f"解析LLM响应失败: {str(e)}, response={response_text}")
|
||||
# 返回默认建议
|
||||
suggestions_response = self._get_default_suggestions(health_data)
|
||||
|
||||
# 8. 验证建议数量(3-5条)
|
||||
if len(suggestions_response.suggestions) < 3:
|
||||
logger.warning(f"建议数量不足: {len(suggestions_response.suggestions)}")
|
||||
suggestions_response = self._get_default_suggestions(health_data)
|
||||
elif len(suggestions_response.suggestions) > 5:
|
||||
logger.warning(f"建议数量过多: {len(suggestions_response.suggestions)}")
|
||||
suggestions_response.suggestions = suggestions_response.suggestions[:5]
|
||||
|
||||
# 9. 格式化响应
|
||||
response = {
|
||||
"health_summary": suggestions_response.health_summary,
|
||||
"suggestions": [
|
||||
{
|
||||
"type": s.type,
|
||||
"title": s.title,
|
||||
"content": s.content,
|
||||
"priority": s.priority,
|
||||
"actionable_steps": s.actionable_steps
|
||||
}
|
||||
for s in suggestions_response.suggestions
|
||||
]
|
||||
}
|
||||
|
||||
logger.info(f"个性化建议生成完成: suggestions_count={len(response['suggestions'])}")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成个性化建议失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def _get_simple_user_profile(self, end_user_id: str) -> Dict[str, Any]:
|
||||
"""获取简化的用户画像数据
|
||||
|
||||
Args:
|
||||
end_user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Dict: 用户画像数据
|
||||
"""
|
||||
try:
|
||||
connector = Neo4jConnector()
|
||||
|
||||
# 查询用户的实体和标签
|
||||
query = """
|
||||
MATCH (e:Entity)
|
||||
WHERE e.group_id = $group_id
|
||||
RETURN e.name as name, e.type as type
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
|
||||
entities = await connector.execute_query(query, group_id=end_user_id)
|
||||
|
||||
# 提取兴趣标签
|
||||
interests = [e["name"] for e in entities if e.get("type") in ["INTEREST", "HOBBY"]][:5]
|
||||
# 后期会引入用户的习惯。。
|
||||
return {
|
||||
"interests": interests if interests else ["未知"]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户画像失败: {str(e)}")
|
||||
return {"interests": ["未知"]}
|
||||
|
||||
async def _build_suggestion_prompt(
|
||||
self,
|
||||
health_data: Dict[str, Any],
|
||||
patterns: Dict[str, Any],
|
||||
user_profile: Dict[str, Any]
|
||||
) -> str:
|
||||
"""构建情绪建议生成的prompt
|
||||
|
||||
Args:
|
||||
health_data: 情绪健康数据
|
||||
patterns: 情绪模式分析结果
|
||||
user_profile: 用户画像数据
|
||||
|
||||
Returns:
|
||||
str: LLM prompt
|
||||
"""
|
||||
from app.core.memory.utils.prompt.prompt_utils import render_emotion_suggestions_prompt
|
||||
|
||||
prompt = await render_emotion_suggestions_prompt(
|
||||
health_data=health_data,
|
||||
patterns=patterns,
|
||||
user_profile=user_profile
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
def _get_default_suggestions(self, health_data: Dict[str, Any]) -> EmotionSuggestionsResponse:
|
||||
"""获取默认建议(当LLM调用失败时使用)
|
||||
|
||||
Args:
|
||||
health_data: 情绪健康数据
|
||||
|
||||
Returns:
|
||||
EmotionSuggestionsResponse: 默认建议
|
||||
"""
|
||||
health_score = health_data.get('health_score', 0)
|
||||
|
||||
if health_score >= 80:
|
||||
summary = "您的情绪健康状况优秀,请继续保持积极的生活态度。"
|
||||
elif health_score >= 60:
|
||||
summary = "您的情绪健康状况良好,可以通过一些调整进一步提升。"
|
||||
elif health_score >= 40:
|
||||
summary = "您的情绪健康需要关注,建议采取一些改善措施。"
|
||||
else:
|
||||
summary = "您的情绪健康需要重点关注,建议寻求专业帮助。"
|
||||
|
||||
suggestions = [
|
||||
EmotionSuggestion(
|
||||
type="emotion_balance",
|
||||
title="保持情绪平衡",
|
||||
content="通过正念冥想和深呼吸练习,帮助您更好地管理情绪波动,提升情绪稳定性。",
|
||||
priority="high",
|
||||
actionable_steps=[
|
||||
"每天早晨进行5-10分钟的正念冥想",
|
||||
"感到情绪波动时,进行3次深呼吸",
|
||||
"记录每天的情绪变化,识别触发因素"
|
||||
]
|
||||
),
|
||||
EmotionSuggestion(
|
||||
type="activity_recommendation",
|
||||
title="增加户外活动",
|
||||
content="适度的户外运动可以有效改善情绪,增强身心健康。建议每周进行3-4次户外活动。",
|
||||
priority="medium",
|
||||
actionable_steps=[
|
||||
"每周安排2-3次30分钟的散步",
|
||||
"周末尝试户外运动如骑行或爬山",
|
||||
"在户外活动时关注周围环境,放松心情"
|
||||
]
|
||||
),
|
||||
EmotionSuggestion(
|
||||
type="social_connection",
|
||||
title="加强社交联系",
|
||||
content="与朋友和家人保持良好的社交联系,可以提供情感支持,改善情绪健康。",
|
||||
priority="medium",
|
||||
actionable_steps=[
|
||||
"每周至少与一位朋友或家人深入交流",
|
||||
"参加感兴趣的社交活动或兴趣小组",
|
||||
"主动分享自己的感受和想法"
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
return EmotionSuggestionsResponse(
|
||||
health_summary=summary,
|
||||
suggestions=suggestions
|
||||
)
|
||||
212
api/app/services/emotion_config_service.py
Normal file
212
api/app/services/emotion_config_service.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""情绪配置服务模块
|
||||
|
||||
本模块提供情绪引擎配置的管理功能,包括获取和更新配置。
|
||||
|
||||
Classes:
|
||||
EmotionConfigService: 情绪配置服务,提供配置管理功能
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.data_config_model import DataConfig
|
||||
from app.core.logging_config import get_business_logger
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
|
||||
class EmotionConfigService:
|
||||
"""情绪配置服务
|
||||
|
||||
提供情绪引擎配置的管理功能,包括:
|
||||
- 获取情绪配置
|
||||
- 更新情绪配置
|
||||
- 验证配置参数
|
||||
|
||||
Attributes:
|
||||
db: 数据库会话
|
||||
"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
"""初始化情绪配置服务
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
"""
|
||||
self.db = db
|
||||
logger.info("情绪配置服务初始化完成")
|
||||
|
||||
def get_emotion_config(self, config_id: int) -> Dict[str, Any]:
|
||||
"""获取情绪引擎配置
|
||||
|
||||
查询指定配置ID的情绪相关配置字段。
|
||||
|
||||
Args:
|
||||
config_id: 配置ID
|
||||
|
||||
Returns:
|
||||
Dict: 包含情绪配置的响应数据:
|
||||
- config_id: 配置ID
|
||||
- emotion_enabled: 是否启用情绪提取
|
||||
- emotion_model_id: 情绪分析专用模型ID
|
||||
- emotion_extract_keywords: 是否提取情绪关键词
|
||||
- emotion_min_intensity: 最小情绪强度阈值
|
||||
- emotion_enable_subject: 是否启用主体分类
|
||||
|
||||
Raises:
|
||||
ValueError: 当配置不存在时
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取情绪配置: config_id={config_id}")
|
||||
|
||||
# 查询配置
|
||||
config = self.db.query(DataConfig).filter(
|
||||
DataConfig.config_id == config_id
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
logger.error(f"配置不存在: config_id={config_id}")
|
||||
raise ValueError(f"配置不存在: config_id={config_id}")
|
||||
|
||||
# 提取情绪相关字段
|
||||
emotion_config = {
|
||||
"config_id": config.config_id,
|
||||
"emotion_enabled": config.emotion_enabled,
|
||||
"emotion_model_id": config.emotion_model_id,
|
||||
"emotion_extract_keywords": config.emotion_extract_keywords,
|
||||
"emotion_min_intensity": config.emotion_min_intensity,
|
||||
"emotion_enable_subject": config.emotion_enable_subject
|
||||
}
|
||||
|
||||
logger.info(f"情绪配置获取成功: config_id={config_id}")
|
||||
return emotion_config
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取情绪配置失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
def validate_emotion_config(self, config_data: Dict[str, Any]) -> bool:
|
||||
"""验证情绪配置参数
|
||||
|
||||
验证配置参数的有效性,包括:
|
||||
- emotion_min_intensity 在 [0.0, 1.0] 范围内
|
||||
- 布尔字段类型正确
|
||||
- emotion_model_id 格式有效(如果提供)
|
||||
|
||||
Args:
|
||||
config_data: 配置数据字典
|
||||
|
||||
Returns:
|
||||
bool: 验证是否通过
|
||||
|
||||
Raises:
|
||||
ValueError: 当配置参数无效时
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"验证情绪配置参数: {config_data}")
|
||||
|
||||
# 验证 emotion_min_intensity 范围
|
||||
if "emotion_min_intensity" in config_data:
|
||||
min_intensity = config_data["emotion_min_intensity"]
|
||||
if not isinstance(min_intensity, (int, float)):
|
||||
raise ValueError("emotion_min_intensity 必须是数字类型")
|
||||
if not (0.0 <= min_intensity <= 1.0):
|
||||
raise ValueError("emotion_min_intensity 必须在 0.0 到 1.0 之间")
|
||||
|
||||
# 验证布尔字段
|
||||
bool_fields = ["emotion_enabled", "emotion_extract_keywords", "emotion_enable_subject"]
|
||||
for field in bool_fields:
|
||||
if field in config_data:
|
||||
value = config_data[field]
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError(f"{field} 必须是布尔类型")
|
||||
|
||||
# 验证 emotion_model_id(如果提供)
|
||||
if "emotion_model_id" in config_data:
|
||||
model_id = config_data["emotion_model_id"]
|
||||
if model_id is not None and not isinstance(model_id, str):
|
||||
raise ValueError("emotion_model_id 必须是字符串类型或 null")
|
||||
if model_id is not None and len(model_id.strip()) == 0:
|
||||
raise ValueError("emotion_model_id 不能为空字符串")
|
||||
|
||||
logger.debug("情绪配置参数验证通过")
|
||||
return True
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"配置参数验证失败: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"验证配置参数时发生错误: {str(e)}", exc_info=True)
|
||||
raise ValueError(f"验证配置参数失败: {str(e)}")
|
||||
|
||||
def update_emotion_config(
|
||||
self,
|
||||
config_id: int,
|
||||
config_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""更新情绪引擎配置
|
||||
|
||||
更新指定配置ID的情绪相关配置字段。
|
||||
|
||||
Args:
|
||||
config_id: 配置ID
|
||||
config_data: 要更新的配置数据,可包含以下字段:
|
||||
- emotion_enabled: 是否启用情绪提取
|
||||
- emotion_model_id: 情绪分析专用模型ID
|
||||
- emotion_extract_keywords: 是否提取情绪关键词
|
||||
- emotion_min_intensity: 最小情绪强度阈值
|
||||
- emotion_enable_subject: 是否启用主体分类
|
||||
|
||||
Returns:
|
||||
Dict: 更新后的完整情绪配置
|
||||
|
||||
Raises:
|
||||
ValueError: 当配置不存在或参数无效时
|
||||
"""
|
||||
try:
|
||||
logger.info(f"更新情绪配置: config_id={config_id}, data={config_data}")
|
||||
|
||||
# 验证配置参数
|
||||
self.validate_emotion_config(config_data)
|
||||
|
||||
# 查询配置
|
||||
config = self.db.query(DataConfig).filter(
|
||||
DataConfig.config_id == config_id
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
logger.error(f"配置不存在: config_id={config_id}")
|
||||
raise ValueError(f"配置不存在: config_id={config_id}")
|
||||
|
||||
# 更新字段
|
||||
if "emotion_enabled" in config_data:
|
||||
config.emotion_enabled = config_data["emotion_enabled"]
|
||||
if "emotion_model_id" in config_data:
|
||||
config.emotion_model_id = config_data["emotion_model_id"]
|
||||
if "emotion_extract_keywords" in config_data:
|
||||
config.emotion_extract_keywords = config_data["emotion_extract_keywords"]
|
||||
if "emotion_min_intensity" in config_data:
|
||||
config.emotion_min_intensity = config_data["emotion_min_intensity"]
|
||||
if "emotion_enable_subject" in config_data:
|
||||
config.emotion_enable_subject = config_data["emotion_enable_subject"]
|
||||
|
||||
# 提交更改
|
||||
self.db.commit()
|
||||
self.db.refresh(config)
|
||||
|
||||
# 返回更新后的配置
|
||||
updated_config = self.get_emotion_config(config_id)
|
||||
|
||||
logger.info(f"情绪配置更新成功: config_id={config_id}")
|
||||
return updated_config
|
||||
|
||||
except ValueError:
|
||||
self.db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"更新情绪配置失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
200
api/app/services/emotion_extraction_service.py
Normal file
200
api/app/services/emotion_extraction_service.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""Emotion extraction service for analyzing emotions from statements.
|
||||
|
||||
This service extracts emotion information from user statements using LLM,
|
||||
including emotion type, intensity, keywords, subject classification, and target.
|
||||
|
||||
Classes:
|
||||
EmotionExtractionService: Service for extracting emotions from statements
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from app.core.memory.models.emotion_models import EmotionExtraction
|
||||
from app.models.data_config_model import DataConfig
|
||||
from app.core.memory.utils.llm.llm_utils import get_llm_client
|
||||
from app.core.memory.llm_tools.llm_client import LLMClientException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmotionExtractionService:
|
||||
"""Service for extracting emotion information from statements.
|
||||
|
||||
This service uses LLM to analyze statements and extract structured emotion
|
||||
information including type, intensity, keywords, subject, and target.
|
||||
It respects configuration settings for enabling/disabling extraction and
|
||||
filtering by intensity threshold.
|
||||
|
||||
Attributes:
|
||||
llm_client: LLM client for making structured output calls
|
||||
"""
|
||||
|
||||
def __init__(self, llm_id: Optional[str] = None):
|
||||
"""Initialize the emotion extraction service.
|
||||
|
||||
Args:
|
||||
llm_id: Optional LLM model ID. If None, uses default from config.
|
||||
"""
|
||||
self.llm_client = None
|
||||
self.llm_id = llm_id
|
||||
logger.info(f"Initialized EmotionExtractionService with llm_id={llm_id}")
|
||||
|
||||
def _get_llm_client(self, model_id: Optional[str] = None):
|
||||
"""Get or create LLM client instance.
|
||||
|
||||
Args:
|
||||
model_id: Optional model ID to use. If None, uses instance llm_id.
|
||||
|
||||
Returns:
|
||||
LLM client instance
|
||||
"""
|
||||
if self.llm_client is None or model_id:
|
||||
effective_model_id = model_id or self.llm_id
|
||||
self.llm_client = get_llm_client(effective_model_id)
|
||||
return self.llm_client
|
||||
|
||||
async def extract_emotion(
|
||||
self,
|
||||
statement: str,
|
||||
config: DataConfig
|
||||
) -> Optional[EmotionExtraction]:
|
||||
"""Extract emotion information from a statement.
|
||||
|
||||
This method checks if emotion extraction is enabled in the config,
|
||||
builds an appropriate prompt, calls the LLM for structured output,
|
||||
and applies intensity threshold filtering.
|
||||
|
||||
Args:
|
||||
statement: The statement text to analyze
|
||||
config: Data configuration object containing emotion settings
|
||||
|
||||
Returns:
|
||||
EmotionExtraction object if extraction succeeds and passes threshold,
|
||||
None if extraction is disabled, fails, or doesn't meet threshold
|
||||
|
||||
Raises:
|
||||
No exceptions are raised - failures are logged and return None
|
||||
"""
|
||||
# Check if emotion extraction is enabled
|
||||
if not config.emotion_enabled:
|
||||
logger.debug("Emotion extraction is disabled in config")
|
||||
return None
|
||||
|
||||
# Validate statement
|
||||
if not statement or not statement.strip():
|
||||
logger.warning("Empty statement provided for emotion extraction")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Build the emotion extraction prompt
|
||||
prompt = await self._build_emotion_prompt(
|
||||
statement=statement,
|
||||
extract_keywords=config.emotion_extract_keywords,
|
||||
enable_subject=config.emotion_enable_subject
|
||||
)
|
||||
|
||||
# Call LLM for structured output
|
||||
emotion = await self._call_llm_structured(
|
||||
prompt=prompt,
|
||||
model_id=config.emotion_model_id
|
||||
)
|
||||
|
||||
# Apply intensity threshold filtering
|
||||
if emotion.emotion_intensity < config.emotion_min_intensity:
|
||||
logger.debug(
|
||||
f"Emotion intensity {emotion.emotion_intensity} below threshold "
|
||||
f"{config.emotion_min_intensity}, skipping storage"
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
f"Successfully extracted emotion: type={emotion.emotion_type}, "
|
||||
f"intensity={emotion.emotion_intensity}, subject={emotion.emotion_subject}"
|
||||
)
|
||||
|
||||
return emotion
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Emotion extraction failed for statement: {statement[:50]}..., "
|
||||
f"error: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
return None
|
||||
|
||||
async def _build_emotion_prompt(
|
||||
self,
|
||||
statement: str,
|
||||
extract_keywords: bool,
|
||||
enable_subject: bool
|
||||
) -> str:
|
||||
"""Build the emotion extraction prompt based on configuration.
|
||||
|
||||
This method constructs a detailed prompt for the LLM that includes
|
||||
instructions for emotion type classification, intensity assessment,
|
||||
and optionally keyword extraction and subject classification.
|
||||
|
||||
Args:
|
||||
statement: The statement to analyze
|
||||
extract_keywords: Whether to extract emotion keywords
|
||||
enable_subject: Whether to enable subject classification
|
||||
|
||||
Returns:
|
||||
Formatted prompt string for LLM
|
||||
"""
|
||||
from app.core.memory.utils.prompt.prompt_utils import render_emotion_extraction_prompt
|
||||
|
||||
prompt = await render_emotion_extraction_prompt(
|
||||
statement=statement,
|
||||
extract_keywords=extract_keywords,
|
||||
enable_subject=enable_subject
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
async def _call_llm_structured(
|
||||
self,
|
||||
prompt: str,
|
||||
model_id: Optional[str] = None
|
||||
) -> EmotionExtraction:
|
||||
"""Call LLM for structured emotion extraction output.
|
||||
|
||||
This method uses the LLM client's response_structured method to get
|
||||
a validated EmotionExtraction object from the LLM.
|
||||
|
||||
Args:
|
||||
prompt: The formatted prompt for emotion extraction
|
||||
model_id: Optional model ID to use for this call
|
||||
|
||||
Returns:
|
||||
EmotionExtraction object with validated emotion data
|
||||
|
||||
Raises:
|
||||
LLMClientException: If LLM call fails or times out
|
||||
ValidationError: If LLM response doesn't match expected schema
|
||||
"""
|
||||
try:
|
||||
# Get LLM client
|
||||
llm_client = self._get_llm_client(model_id)
|
||||
|
||||
# Prepare messages
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# Call LLM with structured output
|
||||
emotion = await llm_client.response_structured(
|
||||
messages=messages,
|
||||
response_model=EmotionExtraction,
|
||||
temperature=0.3,
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
return emotion
|
||||
|
||||
except LLMClientException as e:
|
||||
logger.error(f"LLM call failed: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in LLM structured call: {str(e)}")
|
||||
raise LLMClientException(f"Emotion extraction LLM call failed: {str(e)}")
|
||||
@@ -385,7 +385,7 @@ class LLMRouter:
|
||||
# 获取 API Key 配置
|
||||
api_key_config = self.db.query(ModelApiKey).filter(
|
||||
ModelApiKey.model_config_id == self.routing_model_config.id,
|
||||
ModelApiKey.is_active == True
|
||||
ModelApiKey.is_active
|
||||
).first()
|
||||
|
||||
if not api_key_config:
|
||||
|
||||
397
api/app/services/memory_reflection_service.py
Normal file
397
api/app/services/memory_reflection_service.py
Normal file
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
记忆反思服务
|
||||
处理反思引擎的调用和执行
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, Set
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import get_db
|
||||
from app.core.logging_config import get_api_logger
|
||||
from app.core.memory.storage_services.reflection_engine import ReflectionConfig, ReflectionEngine
|
||||
from app.core.memory.storage_services.reflection_engine.self_reflexion import ReflectionRange, ReflectionBaseline
|
||||
from app.repositories.data_config_repository import DataConfigRepository
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from app.models.app_model import App
|
||||
from app.models.app_release_model import AppRelease
|
||||
from app.models.end_user_model import EndUser
|
||||
|
||||
api_logger = get_api_logger()
|
||||
|
||||
|
||||
class WorkspaceAppService:
|
||||
"""Workplace Application Service Class """
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_workspace_apps_detailed(self, workspace_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get detailed information of all applications in the workspace
|
||||
|
||||
Args:
|
||||
Workspace_id: Workspace ID
|
||||
|
||||
Returns:
|
||||
Dictionary containing detailed application information
|
||||
"""
|
||||
apps = self.db.query(App).filter(App.workspace_id == workspace_id).all()
|
||||
app_ids = [str(app.id) for app in apps]
|
||||
|
||||
apps_detailed_info = []
|
||||
|
||||
for app in apps:
|
||||
app_info = self._build_app_info(app)
|
||||
self._process_app_releases(app, app_info)
|
||||
self._process_end_users(app, app_info)
|
||||
apps_detailed_info.append(app_info)
|
||||
|
||||
return {
|
||||
"status": "成功",
|
||||
"message": f"成功查询到 {len(app_ids)} 个应用及其详细信息",
|
||||
"workspace_id": str(workspace_id),
|
||||
"apps_count": len(app_ids),
|
||||
"app_ids": app_ids,
|
||||
"apps_detailed_info": apps_detailed_info
|
||||
}
|
||||
|
||||
def _build_app_info(self, app: App) -> Dict[str, Any]:
|
||||
"""base_infomation"""
|
||||
return {
|
||||
"id": str(app.id),
|
||||
"name": app.name,
|
||||
"description": app.description,
|
||||
"type": app.type,
|
||||
"status": app.status,
|
||||
"visibility": app.visibility,
|
||||
"created_at": app.created_at.isoformat() if app.created_at else None,
|
||||
"updated_at": app.updated_at.isoformat() if app.updated_at else None,
|
||||
"releases": [],
|
||||
"data_configs": [],
|
||||
"end_users": []
|
||||
}
|
||||
|
||||
def _process_app_releases(self, app: App, app_info: Dict[str, Any]) -> None:
|
||||
"""Process the release version and configuration information of the application"""
|
||||
app_releases = self.db.query(AppRelease).filter(AppRelease.app_id == app.id).all()
|
||||
|
||||
if not app_releases:
|
||||
return
|
||||
|
||||
processed_configs: Set[str] = set()
|
||||
|
||||
for release in app_releases:
|
||||
memory_content = self._extract_memory_content(release.config)
|
||||
|
||||
|
||||
if memory_content and memory_content in processed_configs:
|
||||
continue
|
||||
|
||||
release_info = {
|
||||
"app_id": str(release.app_id),
|
||||
"config": memory_content
|
||||
}
|
||||
|
||||
|
||||
if memory_content:
|
||||
processed_configs.add(memory_content)
|
||||
data_config_info = self._get_data_config(memory_content)
|
||||
|
||||
if data_config_info:
|
||||
if not any(dc["config_id"] == data_config_info["config_id"] for dc in app_info["data_configs"]):
|
||||
app_info["data_configs"].append(data_config_info)
|
||||
|
||||
app_info["releases"].append(release_info)
|
||||
|
||||
def _extract_memory_content(self, config: Any) -> str:
|
||||
"""Extract memory_comtent from config"""
|
||||
if not config or not isinstance(config, dict):
|
||||
return None
|
||||
|
||||
memory_obj = config.get('memory')
|
||||
if memory_obj and isinstance(memory_obj, dict):
|
||||
return memory_obj.get('memory_content')
|
||||
|
||||
return None
|
||||
|
||||
def _get_data_config(self, memory_content: str) -> Dict[str, Any]:
|
||||
"""Retrieve data_comfig information based on memory_comtent"""
|
||||
try:
|
||||
data_config_query, data_config_params = DataConfigRepository.build_select_reflection(memory_content)
|
||||
data_config_result = self.db.execute(text(data_config_query), data_config_params).fetchone()
|
||||
if data_config_result is None:
|
||||
return None
|
||||
|
||||
if data_config_result:
|
||||
return {
|
||||
"config_id": data_config_result.config_id,
|
||||
"enable_self_reflexion": data_config_result.enable_self_reflexion,
|
||||
"iteration_period": data_config_result.iteration_period,
|
||||
"reflexion_range": data_config_result.reflexion_range,
|
||||
"baseline": data_config_result.baseline,
|
||||
"reflection_model_id": data_config_result.reflection_model_id,
|
||||
"memory_verify": data_config_result.memory_verify,
|
||||
"quality_assessment": data_config_result.quality_assessment,
|
||||
"user_id": data_config_result.user_id
|
||||
}
|
||||
except Exception as e:
|
||||
api_logger.warning(f"查询data_config失败,memory_content: {memory_content}, 错误: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
def _process_end_users(self, app: App, app_info: Dict[str, Any]) -> None:
|
||||
"""Processing end-user information for applications"""
|
||||
end_users = self.db.query(EndUser).filter(EndUser.app_id == app.id).all()
|
||||
|
||||
for end_user in end_users:
|
||||
end_user_info = {
|
||||
"id": str(end_user.id),
|
||||
"app_id": str(end_user.app_id)
|
||||
}
|
||||
app_info["end_users"].append(end_user_info)
|
||||
|
||||
def get_end_user_reflection_time(self, end_user_id: str) -> Optional[Any]:
|
||||
"""
|
||||
Read the reflection time of end users
|
||||
|
||||
Args:
|
||||
End_user_id: End User ID
|
||||
|
||||
Returns:
|
||||
Reflection time or None
|
||||
"""
|
||||
try:
|
||||
end_user = self.db.query(EndUser).filter(EndUser.id == end_user_id).first()
|
||||
if end_user:
|
||||
return end_user.reflection_time
|
||||
return None
|
||||
except Exception as e:
|
||||
api_logger.error(f"读取用户反思时间失败,end_user_id: {end_user_id}, 错误: {str(e)}")
|
||||
return None
|
||||
|
||||
def update_end_user_reflection_time(self, end_user_id: str) -> bool:
|
||||
"""
|
||||
Update the reflection time of end users to the current time
|
||||
|
||||
Args:
|
||||
End_user_id: End User ID
|
||||
|
||||
Returns:
|
||||
Is the update successful
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
end_user = self.db.query(EndUser).filter(EndUser.id == end_user_id).first()
|
||||
if end_user:
|
||||
end_user.reflection_time = datetime.now()
|
||||
self.db.commit()
|
||||
api_logger.info(f"成功更新用户反思时间,end_user_id: {end_user_id}")
|
||||
return True
|
||||
else:
|
||||
api_logger.warning(f"未找到用户,end_user_id: {end_user_id}")
|
||||
return False
|
||||
except Exception as e:
|
||||
api_logger.error(f"更新用户反思时间失败,end_user_id: {end_user_id}, 错误: {str(e)}")
|
||||
self.db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
class MemoryReflectionService:
|
||||
"""Memory reflection service category"""
|
||||
|
||||
def __init__(self,db: Session = Depends(get_db)):
|
||||
self.db=db
|
||||
|
||||
|
||||
async def start_reflection_from_data(self, config_data: Dict[str, Any], end_user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Starting Reflection from Configuration Data
|
||||
|
||||
Args:
|
||||
config_data: Configure data dictionary, including reflective configuration information
|
||||
end_user_id: end_user_id
|
||||
|
||||
Returns:
|
||||
Reflect on the execution results
|
||||
"""
|
||||
try:
|
||||
config_id = config_data.get("config_id")
|
||||
api_logger.info(f"从配置数据启动反思,config_id: {config_id}, end_user_id: {end_user_id}")
|
||||
|
||||
|
||||
if not config_data.get("enable_self_reflexion", False):
|
||||
return {
|
||||
"status": "跳过",
|
||||
"message": "反思引擎未启用",
|
||||
"config_id": config_id,
|
||||
"end_user_id": end_user_id,
|
||||
"config_data": config_data
|
||||
}
|
||||
|
||||
|
||||
config_data_id=config_data['config_id']
|
||||
reflection_config=WorkspaceAppService(self.db)._get_data_config(config_data_id)
|
||||
if reflection_config is not None and reflection_config['enable_self_reflexion']:
|
||||
reflection_config= self._create_reflection_config_from_data(reflection_config)
|
||||
iteration_period=reflection_config.iteration_period
|
||||
workspace_service = WorkspaceAppService(self.db)
|
||||
current_reflection_time = workspace_service.get_end_user_reflection_time(end_user_id)
|
||||
|
||||
reflection_time = datetime.fromisoformat(str(current_reflection_time))
|
||||
|
||||
current_time = datetime.now()
|
||||
time_diff = current_time - reflection_time
|
||||
hours_diff = int(time_diff.total_seconds() / 3600)
|
||||
if iteration_period==hours_diff or current_reflection_time is None:
|
||||
api_logger.info(f"与上次的反思时间间隔为: {hours_diff} 小时")
|
||||
# 3. 执行反思引擎
|
||||
reflection_results = await self._execute_reflection_engine(
|
||||
reflection_config, end_user_id
|
||||
)
|
||||
# 更新反思时间为当前时间
|
||||
update_success = workspace_service.update_end_user_reflection_time(end_user_id)
|
||||
if update_success:
|
||||
api_logger.info(f"成功更新用户 {end_user_id} 的反思时间")
|
||||
else:
|
||||
api_logger.error(f"更新用户 {end_user_id} 的反思时间失败")
|
||||
|
||||
return {
|
||||
"status": "完成",
|
||||
"message": "反思引擎执行完成",
|
||||
"config_id": config_id,
|
||||
"end_user_id": end_user_id,
|
||||
"config_data": config_data,
|
||||
"reflection_results": reflection_results
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "等待中..",
|
||||
"message": "反思引擎未开始执行执",
|
||||
"config_id": config_id,
|
||||
"end_user_id": end_user_id,
|
||||
"config_data": config_data,
|
||||
"reflection_results": ''
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
config_id = config_data.get("config_id", "unknown")
|
||||
api_logger.error(f"启动反思失败,config_id: {config_id}, end_user_id: {end_user_id}, 错误: {str(e)}")
|
||||
return {
|
||||
"status": "错误",
|
||||
"message": f"启动反思失败: {str(e)}",
|
||||
"config_id": config_id,
|
||||
"end_user_id": end_user_id,
|
||||
"config_data": config_data
|
||||
}
|
||||
|
||||
def _create_reflection_config_from_data(self, config_data: Dict[str, Any]) -> ReflectionConfig:
|
||||
"""Create reflective configuration objects from configuration data"""
|
||||
|
||||
reflexion_range_value = config_data.get("reflexion_range")
|
||||
if reflexion_range_value is None or reflexion_range_value == "":
|
||||
reflexion_range_value = "partial"
|
||||
reflexion_range = ReflectionRange(reflexion_range_value)
|
||||
|
||||
baseline_value = config_data.get("baseline")
|
||||
if baseline_value is None or baseline_value == "":
|
||||
baseline_value = "TIME"
|
||||
baseline = ReflectionBaseline(baseline_value)
|
||||
|
||||
# iteration_period =
|
||||
iteration_period = config_data.get("iteration_period", 24)
|
||||
if isinstance(iteration_period, str):
|
||||
try:
|
||||
iteration_period = int(iteration_period)
|
||||
except (ValueError, TypeError):
|
||||
iteration_period = 24 # 默认24小时
|
||||
|
||||
return ReflectionConfig(
|
||||
enabled=config_data.get("enable_self_reflexion", False),
|
||||
iteration_period=str(iteration_period), # ReflectionConfig期望字符串
|
||||
reflexion_range=reflexion_range,
|
||||
baseline=baseline,
|
||||
memory_verify=config_data.get("memory_verify", False),
|
||||
quality_assessment=config_data.get("quality_assessment", False),
|
||||
model_id=config_data.get("reflection_model_id", "")
|
||||
)
|
||||
|
||||
async def _execute_reflection_engine(
|
||||
self,
|
||||
reflection_config: ReflectionConfig,
|
||||
user_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute Reflection Engine"""
|
||||
try:
|
||||
# 创建Neo4j连接器
|
||||
connector = Neo4jConnector()
|
||||
|
||||
# 创建反思引擎
|
||||
engine = ReflectionEngine(
|
||||
config=reflection_config,
|
||||
neo4j_connector=connector,
|
||||
llm_client=reflection_config.model_id
|
||||
)
|
||||
|
||||
# 执行反思
|
||||
reflection_result = await engine.execute_reflection(user_id)
|
||||
|
||||
return {
|
||||
"success": reflection_result.success,
|
||||
"message": reflection_result.message,
|
||||
"conflicts_found": reflection_result.conflicts_found,
|
||||
"conflicts_resolved": reflection_result.conflicts_resolved,
|
||||
"memories_updated": reflection_result.memories_updated,
|
||||
"execution_time": reflection_result.execution_time,
|
||||
"details": reflection_result.details
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"反思引擎执行失败: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"反思引擎执行失败: {str(e)}",
|
||||
"conflicts_found": 0,
|
||||
"conflicts_resolved": 0,
|
||||
"memories_updated": 0,
|
||||
"execution_time": 0.0
|
||||
}
|
||||
|
||||
|
||||
class Memory_Reflection_Service:
|
||||
"""Memory Reflection Service - Used for calling the/reflection interface"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.reflection_service = MemoryReflectionService(db)
|
||||
|
||||
async def start_reflection(self, config_data: Dict[str, Any], end_user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Activate the reflection function
|
||||
|
||||
Args:
|
||||
config_data: 配置数据,格式如下:
|
||||
{
|
||||
"config_id": 26,
|
||||
"enable_self_reflexion": true,
|
||||
"iteration_period": "6",
|
||||
"reflexion_range": "partial",
|
||||
"baseline": "TIME",
|
||||
"reflection_model_id": "ea405fa6-c387-4d78-80ab-826d692301b3",
|
||||
"memory_verify": true,
|
||||
"quality_assessment": false,
|
||||
"user_id": null
|
||||
}
|
||||
end_user_id: end_user_id,example "12a8b235-6eb1-4481-a53c-b77933b5c949"
|
||||
|
||||
Returns:
|
||||
"""
|
||||
api_logger.info(f"Memory_Reflection_Service启动反思,config_id: {config_data.get('config_id')}, end_user_id: {end_user_id}")
|
||||
|
||||
# 调用核心反思服务
|
||||
result = await self.reflection_service.start_reflection_from_data(config_data, end_user_id)
|
||||
|
||||
return result
|
||||
278
api/app/services/prompt_optimizer_service.py
Normal file
278
api/app/services/prompt_optimizer_service.py
Normal file
@@ -0,0 +1,278 @@
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.core.models import RedBearModelConfig
|
||||
from app.core.models.llm import RedBearLLM
|
||||
from app.models import ModelConfig, ModelApiKey, ModelType, PromptOptimizerSessionHistory
|
||||
from app.models.prompt_optimizer_model import (
|
||||
PromptOptimizerSession,
|
||||
RoleType
|
||||
)
|
||||
from app.repositories.model_repository import ModelConfigRepository
|
||||
from app.repositories.prompt_optimizer_repository import (
|
||||
PromptOptimizerSessionRepository
|
||||
)
|
||||
from app.schemas.prompt_optimizer_schema import OptimizePromptResult
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
|
||||
class PromptOptimizerService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_model_config(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
model_id: uuid.UUID
|
||||
) -> ModelConfig:
|
||||
"""
|
||||
Retrieve the model configuration for a specific tenant.
|
||||
|
||||
This method fetches the model configuration associated with the given
|
||||
tenant_id and model_id. If no configuration is found, a BusinessException
|
||||
is raised.
|
||||
|
||||
Args:
|
||||
tenant_id (uuid.UUID): The unique identifier of the tenant.
|
||||
model_id (uuid.UUID): The unique identifier of the model.
|
||||
|
||||
Returns:
|
||||
ModelConfig: The corresponding model configuration object.
|
||||
|
||||
Raises:
|
||||
BusinessException: If the model configuration does not exist.
|
||||
"""
|
||||
|
||||
model = ModelConfigRepository.get_by_id(
|
||||
self.db, model_id, tenant_id=tenant_id
|
||||
)
|
||||
if not model:
|
||||
raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND)
|
||||
|
||||
return model
|
||||
|
||||
def create_session(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID
|
||||
) -> PromptOptimizerSession:
|
||||
"""
|
||||
Create a new prompt optimization session.
|
||||
|
||||
This method initializes a new prompt optimization session for the specified
|
||||
tenant, application, and user, and persists it to the database.
|
||||
|
||||
Args:
|
||||
tenant_id (uuid.UUID): The unique identifier of the tenant.
|
||||
user_id (uuid.UUID): The unique identifier of the user.
|
||||
|
||||
Returns:
|
||||
PromptOptimzerSession: The newly created prompt optimization session.
|
||||
"""
|
||||
session = PromptOptimizerSessionRepository(self.db).create_session(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id
|
||||
)
|
||||
return session
|
||||
|
||||
def get_session_message_history(
|
||||
self,
|
||||
session_id: uuid.UUID,
|
||||
user_id: uuid.UUID
|
||||
) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Retrieve the chronological message history for a prompt optimization session.
|
||||
|
||||
This method queries the database to fetch all messages associated with a
|
||||
specific prompt optimization session for a given user. Messages are returned
|
||||
in chronological order and typically include both user inputs and
|
||||
model-generated responses.
|
||||
|
||||
Args:
|
||||
session_id (uuid.UUID): The unique identifier of the prompt optimization session.
|
||||
user_id (uuid.UUID): The unique identifier of the user associated with the session.
|
||||
|
||||
Returns:
|
||||
list[tuple[str, str]]: A list of tuples representing messages. Each tuple contains:
|
||||
- role (str): The role of the message sender, e.g., 'system', 'user', or 'assistant'.
|
||||
- content (str): The content of the message.
|
||||
"""
|
||||
history = PromptOptimizerSessionRepository(self.db).get_session_history(
|
||||
session_id=session_id,
|
||||
user_id=user_id
|
||||
)
|
||||
messages = []
|
||||
for message in history:
|
||||
messages.append((message.role, message.content))
|
||||
return messages
|
||||
|
||||
async def optimize_prompt(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
model_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
current_prompt: str,
|
||||
user_require: str
|
||||
) -> OptimizePromptResult:
|
||||
"""
|
||||
Optimize a user-provided prompt using a configured prompt optimizer LLM.
|
||||
|
||||
This method refines the original prompt according to the user's requirements,
|
||||
generating an optimized version that is directly usable by AI tools. The
|
||||
optimization process follows strict rules, including:
|
||||
- Wrapping user-inserted variables in double curly braces {{}}.
|
||||
- Adhering to Jinja2 variable syntax if applicable.
|
||||
- Ensuring a clear logic flow, explicit instructions, and strong executability.
|
||||
- Producing output in a strict JSON format.
|
||||
|
||||
Steps performed:
|
||||
1. Retrieve the model configuration for the given tenant and model.
|
||||
2. Fetch the session message history for context.
|
||||
3. Instantiate the LLM with the appropriate API key and model configuration.
|
||||
4. Build system messages outlining optimization rules.
|
||||
5. Format the user's original prompt and requirements as a user message.
|
||||
6. Send messages to the LLM to generate the optimized prompt.
|
||||
7. Generate a concise description summarizing the changes made during optimization.
|
||||
|
||||
Args:
|
||||
tenant_id (uuid.UUID): Tenant identifier.
|
||||
model_id (uuid.UUID): Prompt optimizer model identifier.
|
||||
session_id (uuid.UUID): Prompt optimization session identifier.
|
||||
user_id (uuid.UUID): Identifier of the user associated with the session.
|
||||
current_prompt (str): Original prompt to optimize.
|
||||
user_require (str): User's requirements or instructions for optimization.
|
||||
|
||||
Returns:
|
||||
OptimizePromptResult: An object containing:
|
||||
- prompt: The optimized prompt string.
|
||||
- desc: A short description summarizing the changes.
|
||||
|
||||
Raises:
|
||||
BusinessException: If the LLM response cannot be parsed as valid JSON
|
||||
or does not conform to the expected output format.
|
||||
"""
|
||||
model_config = self.get_model_config(tenant_id, model_id)
|
||||
session_history = self.get_session_message_history(session_id=session_id, user_id=user_id)
|
||||
|
||||
# Create LLM instance
|
||||
api_config: ModelApiKey = model_config.api_keys[0]
|
||||
llm = RedBearLLM(RedBearModelConfig(
|
||||
model_name=api_config.model_name,
|
||||
provider=api_config.provider,
|
||||
api_key=api_config.api_key,
|
||||
base_url=api_config.api_base
|
||||
), type=ModelType.from_str(model_config.type))
|
||||
|
||||
# build message
|
||||
messages = [
|
||||
# init system_prompt
|
||||
(
|
||||
RoleType.SYSTEM.value,
|
||||
"Your task is to optimize the original prompt provided by the user so that it can be directly used by AI tools,"
|
||||
"and the variables that the user needs to insert must be wrapped in {{}}. "
|
||||
"The optimized prompt should align with the optimization direction specified by the user (if any) and ensure clear logic, explicit instructions, and strong executability. "
|
||||
"Please follow these rules when optimizing: "
|
||||
'1. Ensure variables are wrapped in {{}}, e.g., optimize "Please enter your question" to "Please enter your {{question}}"'
|
||||
"2. Instructions must be specific and operable, avoiding vague expressions"
|
||||
"3. If the original prompt lacks key elements (such as output format requirements), supplement them completely "
|
||||
"4. Keep the language concise and avoid redundancy "
|
||||
"5. If the user does not specify an optimization direction, the default optimization is to make the prompt structurally clear and with explicit instructions"
|
||||
"Please directly output the optimized prompt without additional explanations. The optimized prompt should be directly usable with correct variable positions."
|
||||
),
|
||||
|
||||
# base model limit
|
||||
(RoleType.SYSTEM.value,
|
||||
"Optimization Rules:\n"
|
||||
"1. Fully adjust the prompt content according to the user's requirements.\n"
|
||||
"When variables are required, use double curly braces {{variable_name}} as placeholders."
|
||||
"Variable names must be derived from the user's requirements.\n"
|
||||
"3. Keep the prompt logic clear and instructions explicit.\n"
|
||||
"4. Ensure that the modified prompt can be directly used.\n\n")
|
||||
]
|
||||
messages.extend(session_history[:-1]) # last message is current message
|
||||
user_message_template = ChatPromptTemplate.from_messages([
|
||||
(RoleType.USER.value, "[original_prompt]\n{current_prompt}\n[user_require]\n{user_require}")
|
||||
])
|
||||
formatted_user_message = user_message_template.format(current_prompt=current_prompt, user_require=user_require)
|
||||
messages.extend([(RoleType.USER.value, formatted_user_message)])
|
||||
logger.info(f"Prompt optimization message: {messages}")
|
||||
optim_prompt = await llm.ainvoke(messages)
|
||||
optim_desc = [
|
||||
(
|
||||
RoleType.SYSTEM.value,
|
||||
"You are a prompt optimization assistant.\n"
|
||||
"Compare the original prompt, the user's requirements, "
|
||||
"and the optimized prompt.\n"
|
||||
"Summarize the changes made during optimization.\n\n"
|
||||
"Rules:\n"
|
||||
"1. Output must be a single short sentence.\n"
|
||||
"2. Be concise and factual.\n"
|
||||
"3. Do not explain the prompts themselves.\n"
|
||||
"4. Do not include any extra text."
|
||||
),
|
||||
(
|
||||
"[Original Prompt]\n"
|
||||
f"{current_prompt}\n\n"
|
||||
"[User Requirements]\n"
|
||||
f"{user_require}\n\n"
|
||||
"[Optimized Prompt]\n"
|
||||
f"{optim_prompt.content}"
|
||||
)
|
||||
]
|
||||
optim_desc = await llm.ainvoke(optim_desc)
|
||||
|
||||
return OptimizePromptResult(
|
||||
prompt=optim_prompt.content,
|
||||
desc=optim_desc.content
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parser_prompt_variables(prompt: str):
|
||||
try:
|
||||
pattern = r'\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
|
||||
matches = re.findall(pattern, prompt)
|
||||
variables = list(set(matches))
|
||||
return variables
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse prompt variables - Error: {str(e)}", exc_info=True)
|
||||
raise BusinessException("Failed to parse prompt variables", BizCode.PARSER_NOT_SUPPORTED)
|
||||
|
||||
@staticmethod
|
||||
def fill_prompt_variables(prompt: str, variables: dict[str, str]):
|
||||
try:
|
||||
pattern = r'\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
|
||||
|
||||
def replace_var(match):
|
||||
var_name = match.group(1)
|
||||
return variables.get(var_name, match.group(0))
|
||||
result = re.sub(pattern, replace_var, prompt)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fill prompt variables - Error: {str(e)}", exc_info=True)
|
||||
raise BusinessException("Failed to fill prompt variables", BizCode.PARSER_NOT_SUPPORTED)
|
||||
|
||||
def create_message(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
role: RoleType,
|
||||
content: str
|
||||
) -> PromptOptimizerSessionHistory:
|
||||
"""Insert Message to Session History"""
|
||||
message = PromptOptimizerSessionRepository(self.db).create_message(
|
||||
tenant_id=tenant_id,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
role=role,
|
||||
content=content
|
||||
)
|
||||
return message
|
||||
@@ -1,29 +1,28 @@
|
||||
"""
|
||||
工作流服务层
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import datetime
|
||||
from typing import Any, Annotated
|
||||
from typing import Any, Annotated, AsyncGenerator
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.workflow.validator import validate_workflow_config
|
||||
from app.db import get_db
|
||||
from app.models.workflow_model import WorkflowConfig, WorkflowExecution
|
||||
from app.repositories.workflow_repository import (
|
||||
WorkflowConfigRepository,
|
||||
WorkflowExecutionRepository,
|
||||
WorkflowNodeExecutionRepository,
|
||||
get_workflow_config_repository,
|
||||
get_workflow_execution_repository,
|
||||
get_workflow_node_execution_repository
|
||||
WorkflowNodeExecutionRepository
|
||||
)
|
||||
from app.core.workflow.validator import validate_workflow_config
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.error_codes import BizCode
|
||||
from app.db import get_db
|
||||
from app.schemas import DraftRunRequest
|
||||
from app.utils.sse_utils import format_sse_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -81,7 +80,7 @@ class WorkflowService:
|
||||
if not is_valid:
|
||||
logger.warning(f"工作流配置验证失败: {errors}")
|
||||
raise BusinessException(
|
||||
error_code=BizCode.INVALID_PARAMETER,
|
||||
code=BizCode.INVALID_PARAMETER,
|
||||
message=f"工作流配置无效: {'; '.join(errors)}"
|
||||
)
|
||||
|
||||
@@ -140,7 +139,7 @@ class WorkflowService:
|
||||
config = self.get_workflow_config(app_id)
|
||||
if not config:
|
||||
raise BusinessException(
|
||||
error_code=BizCode.RESOURCE_NOT_FOUND,
|
||||
code=BizCode.NOT_FOUND,
|
||||
message=f"工作流配置不存在: app_id={app_id}"
|
||||
)
|
||||
|
||||
@@ -166,7 +165,7 @@ class WorkflowService:
|
||||
if not is_valid:
|
||||
logger.warning(f"工作流配置验证失败: {errors}")
|
||||
raise BusinessException(
|
||||
error_code=BizCode.INVALID_PARAMETER,
|
||||
code=BizCode.INVALID_PARAMETER,
|
||||
message=f"工作流配置无效: {'; '.join(errors)}"
|
||||
)
|
||||
|
||||
@@ -195,8 +194,7 @@ class WorkflowService:
|
||||
config = self.get_workflow_config(app_id)
|
||||
if not config:
|
||||
return False
|
||||
|
||||
self.config_repo.delete(config.id)
|
||||
config.is_active = False
|
||||
logger.info(f"删除工作流配置成功: app_id={app_id}, config_id={config.id}")
|
||||
return True
|
||||
|
||||
@@ -245,7 +243,7 @@ class WorkflowService:
|
||||
config = self.get_workflow_config(app_id)
|
||||
if not config:
|
||||
raise BusinessException(
|
||||
error_code=BizCode.RESOURCE_NOT_FOUND,
|
||||
code=BizCode.NOT_FOUND,
|
||||
message=f"工作流配置不存在: app_id={app_id}"
|
||||
)
|
||||
|
||||
@@ -359,7 +357,7 @@ class WorkflowService:
|
||||
execution = self.get_execution(execution_id)
|
||||
if not execution:
|
||||
raise BusinessException(
|
||||
error_code=BizCode.RESOURCE_NOT_FOUND,
|
||||
code=BizCode.NOT_FOUND,
|
||||
message=f"执行记录不存在: execution_id={execution_id}"
|
||||
)
|
||||
|
||||
@@ -438,7 +436,7 @@ class WorkflowService:
|
||||
message=f"工作流配置不存在: app_id={app_id}"
|
||||
)
|
||||
input_data = {"message": payload.message, "variables": payload.variables, "conversation_id": payload.conversation_id}
|
||||
|
||||
|
||||
# 转换 user_id 为 UUID
|
||||
triggered_by_uuid = None
|
||||
if payload.user_id:
|
||||
@@ -446,7 +444,7 @@ class WorkflowService:
|
||||
triggered_by_uuid = uuid.UUID(payload.user_id)
|
||||
except (ValueError, AttributeError):
|
||||
logger.warning(f"无效的 user_id 格式: {payload.user_id}")
|
||||
|
||||
|
||||
# 转换 conversation_id 为 UUID
|
||||
conversation_id_uuid = None
|
||||
if payload.conversation_id:
|
||||
@@ -454,7 +452,7 @@ class WorkflowService:
|
||||
conversation_id_uuid = uuid.UUID(payload.conversation_id)
|
||||
except (ValueError, AttributeError):
|
||||
logger.warning(f"无效的 conversation_id 格式: {payload.conversation_id}")
|
||||
|
||||
|
||||
# 2. 创建执行记录
|
||||
execution = self.create_execution(
|
||||
workflow_config_id=config.id,
|
||||
@@ -474,11 +472,9 @@ class WorkflowService:
|
||||
}
|
||||
|
||||
# 4. 获取工作空间 ID(从 app 获取)
|
||||
from app.models import App
|
||||
|
||||
|
||||
# 5. 执行工作流
|
||||
from app.core.workflow.executor import execute_workflow, execute_workflow_stream
|
||||
from app.core.workflow.executor import execute_workflow
|
||||
|
||||
try:
|
||||
# 更新状态为运行中
|
||||
@@ -530,6 +526,105 @@ class WorkflowService:
|
||||
message=f"工作流执行失败: {str(e)}"
|
||||
)
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
payload: DraftRunRequest,
|
||||
config: WorkflowConfig
|
||||
):
|
||||
"""运行工作流(流式)
|
||||
|
||||
Args:
|
||||
app_id: 应用 ID
|
||||
payload: 请求对象(包含 message, variables, conversation_id 等)
|
||||
config: 存储类型(可选)
|
||||
|
||||
Yields:
|
||||
SSE 格式的流式事件
|
||||
|
||||
Raises:
|
||||
BusinessException: 配置不存在或执行失败时抛出
|
||||
"""
|
||||
# 1. 获取工作流配置
|
||||
if not config:
|
||||
config = self.get_workflow_config(app_id)
|
||||
if not config:
|
||||
raise BusinessException(
|
||||
code=BizCode.CONFIG_MISSING,
|
||||
message=f"工作流配置不存在: app_id={app_id}"
|
||||
)
|
||||
input_data = {"message": payload.message, "variables": payload.variables,
|
||||
"conversation_id": payload.conversation_id}
|
||||
|
||||
# 转换 user_id 为 UUID
|
||||
triggered_by_uuid = None
|
||||
if payload.user_id:
|
||||
try:
|
||||
triggered_by_uuid = uuid.UUID(payload.user_id)
|
||||
except (ValueError, AttributeError):
|
||||
logger.warning(f"无效的 user_id 格式: {payload.user_id}")
|
||||
|
||||
# 转换 conversation_id 为 UUID
|
||||
conversation_id_uuid = None
|
||||
if payload.conversation_id:
|
||||
try:
|
||||
conversation_id_uuid = uuid.UUID(payload.conversation_id)
|
||||
except (ValueError, AttributeError):
|
||||
logger.warning(f"无效的 conversation_id 格式: {payload.conversation_id}")
|
||||
|
||||
# 2. 创建执行记录
|
||||
execution = self.create_execution(
|
||||
workflow_config_id=config.id,
|
||||
app_id=app_id,
|
||||
trigger_type="manual",
|
||||
triggered_by=triggered_by_uuid,
|
||||
conversation_id=conversation_id_uuid,
|
||||
input_data=input_data
|
||||
)
|
||||
|
||||
# 3. 构建工作流配置字典
|
||||
workflow_config_dict = {
|
||||
"nodes": config.nodes,
|
||||
"edges": config.edges,
|
||||
"variables": config.variables,
|
||||
"execution_config": config.execution_config
|
||||
}
|
||||
|
||||
# 4. 获取工作空间 ID(从 app 获取)
|
||||
|
||||
# 5. 流式执行工作流
|
||||
|
||||
try:
|
||||
# 更新状态为运行中
|
||||
self.update_execution_status(execution.execution_id, "running")
|
||||
|
||||
# 调用流式执行(executor 会发送 workflow_start 和 workflow_end 事件)
|
||||
async for event in self._run_workflow_stream(
|
||||
workflow_config=workflow_config_dict,
|
||||
input_data=input_data,
|
||||
execution_id=execution.execution_id,
|
||||
workspace_id="",
|
||||
user_id=payload.user_id
|
||||
):
|
||||
# 直接转发 executor 的事件(已经是正确的格式)
|
||||
yield event
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"工作流流式执行失败: execution_id={execution.execution_id}, error={e}", exc_info=True)
|
||||
self.update_execution_status(
|
||||
execution.execution_id,
|
||||
"failed",
|
||||
error_message=str(e)
|
||||
)
|
||||
# 发送错误事件
|
||||
yield {
|
||||
"event": "error",
|
||||
"data": {
|
||||
"execution_id": execution.execution_id,
|
||||
"error": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
async def run_workflow(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
@@ -537,7 +632,7 @@ class WorkflowService:
|
||||
triggered_by: uuid.UUID,
|
||||
conversation_id: uuid.UUID | None = None,
|
||||
stream: bool = False
|
||||
):
|
||||
) -> AsyncGenerator | dict:
|
||||
"""运行工作流
|
||||
|
||||
Args:
|
||||
@@ -557,7 +652,7 @@ class WorkflowService:
|
||||
config = self.get_workflow_config(app_id)
|
||||
if not config:
|
||||
raise BusinessException(
|
||||
error_code=BizCode.RESOURCE_NOT_FOUND,
|
||||
code=BizCode.NOT_FOUND,
|
||||
message=f"工作流配置不存在: app_id={app_id}"
|
||||
)
|
||||
|
||||
@@ -584,12 +679,12 @@ class WorkflowService:
|
||||
app = self.db.query(App).filter(App.id == app_id).first()
|
||||
if not app:
|
||||
raise BusinessException(
|
||||
error_code=BizCode.RESOURCE_NOT_FOUND,
|
||||
code=BizCode.NOT_FOUND,
|
||||
message=f"应用不存在: app_id={app_id}"
|
||||
)
|
||||
|
||||
# 5. 执行工作流
|
||||
from app.core.workflow.executor import execute_workflow, execute_workflow_stream
|
||||
from app.core.workflow.executor import execute_workflow
|
||||
|
||||
try:
|
||||
# 更新状态为运行中
|
||||
@@ -647,18 +742,48 @@ class WorkflowService:
|
||||
error_message=str(e)
|
||||
)
|
||||
raise BusinessException(
|
||||
error_code=BizCode.INTERNAL_ERROR,
|
||||
code=BizCode.INTERNAL_ERROR,
|
||||
message=f"工作流执行失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _clean_event_for_json(self, event: dict[str, Any]) -> dict[str, Any]:
|
||||
"""清理事件数据,移除不可序列化的对象
|
||||
|
||||
Args:
|
||||
event: 原始事件数据
|
||||
|
||||
Returns:
|
||||
可序列化的事件数据
|
||||
"""
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
def clean_value(value):
|
||||
"""递归清理值"""
|
||||
if isinstance(value, BaseMessage):
|
||||
# 将 Message 对象转换为字典
|
||||
return {
|
||||
"type": value.__class__.__name__,
|
||||
"content": value.content,
|
||||
}
|
||||
elif isinstance(value, dict):
|
||||
return {k: clean_value(v) for k, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
return [clean_value(item) for item in value]
|
||||
elif isinstance(value, (str, int, float, bool, type(None))):
|
||||
return value
|
||||
else:
|
||||
# 其他不可序列化的对象转换为字符串
|
||||
return str(value)
|
||||
|
||||
return clean_value(event)
|
||||
|
||||
async def _run_workflow_stream(
|
||||
self,
|
||||
workflow_config: dict[str, Any],
|
||||
input_data: dict[str, Any],
|
||||
execution_id: str,
|
||||
workspace_id: str,
|
||||
user_id: str
|
||||
):
|
||||
user_id: str):
|
||||
"""运行工作流(流式,内部方法)
|
||||
|
||||
Args:
|
||||
@@ -669,13 +794,11 @@ class WorkflowService:
|
||||
user_id: 用户 ID
|
||||
|
||||
Yields:
|
||||
流式事件
|
||||
流式事件(格式:{"event": "<type>", "data": {...}})
|
||||
"""
|
||||
from app.core.workflow.executor import execute_workflow_stream
|
||||
|
||||
try:
|
||||
output_data = {}
|
||||
|
||||
async for event in execute_workflow_stream(
|
||||
workflow_config=workflow_config,
|
||||
input_data=input_data,
|
||||
@@ -683,31 +806,9 @@ class WorkflowService:
|
||||
workspace_id=workspace_id,
|
||||
user_id=user_id
|
||||
):
|
||||
# 转发事件
|
||||
# 直接转发事件(executor 已经返回正确格式)
|
||||
yield event
|
||||
|
||||
# 收集输出数据
|
||||
if event.get("type") == "node_complete":
|
||||
node_data = event.get("data", {})
|
||||
node_outputs = node_data.get("node_outputs", {})
|
||||
output_data.update(node_outputs)
|
||||
|
||||
# 处理完成事件
|
||||
if event.get("type") == "workflow_complete":
|
||||
self.update_execution_status(
|
||||
execution_id,
|
||||
"completed",
|
||||
output_data=output_data
|
||||
)
|
||||
|
||||
# 处理错误事件
|
||||
if event.get("type") == "workflow_error":
|
||||
self.update_execution_status(
|
||||
execution_id,
|
||||
"failed",
|
||||
error_message=event.get("error")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"工作流流式执行失败: execution_id={execution_id}, error={e}", exc_info=True)
|
||||
self.update_execution_status(
|
||||
@@ -716,9 +817,11 @@ class WorkflowService:
|
||||
error_message=str(e)
|
||||
)
|
||||
yield {
|
||||
"type": "workflow_error",
|
||||
"execution_id": execution_id,
|
||||
"error": str(e)
|
||||
"event": "error",
|
||||
"data": {
|
||||
"execution_id": execution_id,
|
||||
"error": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user