Merge branch 'develop' into fix/memory-enduser-config

This commit is contained in:
Ke Sun
2026-02-03 19:40:08 +08:00
174 changed files with 7483 additions and 4006 deletions

View File

@@ -454,7 +454,8 @@ async def draft_run(
user_id=payload.user_id or str(current_user.id),
variables=payload.variables,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
user_rag_memory_id=user_rag_memory_id,
files=payload.files # 传递多模态文件
):
yield event
@@ -475,7 +476,8 @@ async def draft_run(
"app_id": str(app_id),
"message_length": len(payload.message),
"has_conversation_id": bool(payload.conversation_id),
"has_variables": bool(payload.variables)
"has_variables": bool(payload.variables),
"has_files": bool(payload.files)
}
)
@@ -490,7 +492,8 @@ async def draft_run(
user_id=payload.user_id or str(current_user.id),
variables=payload.variables,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id
user_rag_memory_id=user_rag_memory_id,
files=payload.files # 传递多模态文件
)
logger.debug(

View File

@@ -11,6 +11,7 @@ Routes:
"""
from app.core.error_codes import BizCode
from app.core.language_utils import get_language_from_header
from app.core.logging_config import get_api_logger
from app.core.response_utils import fail, success
from app.dependencies import get_current_user, get_db
@@ -45,11 +46,14 @@ emotion_service = EmotionAnalyticsService()
@router.post("/tags", response_model=ApiResponse)
async def get_emotion_tags(
request: EmotionTagsRequest,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
):
try:
# 使用集中化的语言校验
language = get_language_from_header(language_type)
api_logger.info(
f"用户 {current_user.username} 请求获取情绪标签统计",
extra={
@@ -57,7 +61,8 @@ async def get_emotion_tags(
"emotion_type": request.emotion_type,
"start_date": request.start_date,
"end_date": request.end_date,
"limit": request.limit
"limit": request.limit,
"language_type": language
}
)
@@ -67,7 +72,8 @@ async def get_emotion_tags(
emotion_type=request.emotion_type,
start_date=request.start_date,
end_date=request.end_date,
limit=request.limit
limit=request.limit,
language=language
)
api_logger.info(
@@ -97,11 +103,14 @@ async def get_emotion_tags(
@router.post("/wordcloud", response_model=ApiResponse)
async def get_emotion_wordcloud(
request: EmotionWordcloudRequest,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
):
try:
# 使用集中化的语言校验
language = get_language_from_header(language_type)
api_logger.info(
f"用户 {current_user.username} 请求获取情绪词云数据",
extra={
@@ -144,11 +153,14 @@ async def get_emotion_wordcloud(
@router.post("/health", response_model=ApiResponse)
async def get_emotion_health(
request: EmotionHealthRequest,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
):
try:
# 使用集中化的语言校验
language = get_language_from_header(language_type)
# 验证时间范围参数
if request.time_range not in ["7d", "30d", "90d"]:
raise HTTPException(
@@ -199,7 +211,7 @@ async def get_emotion_health(
@router.post("/suggestions", response_model=ApiResponse)
async def get_emotion_suggestions(
request: EmotionSuggestionsRequest,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
@@ -214,6 +226,9 @@ async def get_emotion_suggestions(
缓存的个性化情绪建议响应
"""
try:
# 使用集中化的语言校验
language = get_language_from_header(language_type)
api_logger.info(
f"用户 {current_user.username} 请求获取个性化情绪建议(缓存)",
extra={
@@ -265,7 +280,7 @@ async def get_emotion_suggestions(
@router.post("/generate_suggestions", response_model=ApiResponse)
async def generate_emotion_suggestions(
request: EmotionGenerateSuggestionsRequest,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
@@ -280,6 +295,9 @@ async def generate_emotion_suggestions(
新生成的个性化情绪建议响应
"""
try:
# 使用集中化的语言校验
language = get_language_from_header(language_type)
api_logger.info(
f"用户 {current_user.username} 请求生成个性化情绪建议",
extra={
@@ -290,7 +308,8 @@ async def generate_emotion_suggestions(
# 调用服务层生成建议
data = await emotion_service.generate_emotion_suggestions(
end_user_id=request.end_user_id,
db=db
db=db,
language=language
)
# 保存到缓存

View File

@@ -2,6 +2,7 @@ from typing import List, Optional
from app.celery_app import celery_app
from app.core.error_codes import BizCode
from app.core.language_utils import get_language_from_header
from app.core.logging_config import get_api_logger
from app.core.rag.llm.cv_model import QWenCV
from app.core.response_utils import fail, success
@@ -118,6 +119,7 @@ async def download_log(
@cur_workspace_access_guard()
async def write_server(
user_input: Write_UserInput,
language_type: str = Header(default=None, alias="X-Language-Type"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
@@ -126,13 +128,17 @@ async def write_server(
Args:
user_input: Write request containing message and end_user_id
language_type: 语言类型 ("zh" 中文, "en" 英文),通过 X-Language-Type Header 传递
Returns:
Response with write operation status
"""
# 使用集中化的语言校验
language = get_language_from_header(language_type)
config_id = user_input.config_id
workspace_id = current_user.current_workspace_id
api_logger.info(f"Write service: workspace_id={workspace_id}, config_id={config_id}")
api_logger.info(f"Write service: workspace_id={workspace_id}, config_id={config_id}, language_type={language}")
# 获取 storage_type如果为 None 则使用默认值
storage_type = workspace_service.get_workspace_storage_type(
@@ -169,7 +175,8 @@ async def write_server(
config_id,
db,
storage_type,
user_rag_memory_id
user_rag_memory_id,
language
)
return success(data=result, msg="写入成功")
@@ -188,6 +195,7 @@ async def write_server(
@cur_workspace_access_guard()
async def write_server_async(
user_input: Write_UserInput,
language_type: str = Header(default=None, alias="X-Language-Type"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
@@ -196,14 +204,18 @@ async def write_server_async(
Args:
user_input: Write request containing message and end_user_id
language_type: 语言类型 ("zh" 中文, "en" 英文),通过 X-Language-Type Header 传递
Returns:
Task ID for tracking async operation
Use GET /memory/write_result/{task_id} to check task status and get result
"""
# 使用集中化的语言校验
language = get_language_from_header(language_type)
config_id = user_input.config_id
workspace_id = current_user.current_workspace_id
api_logger.info(f"Async write service: workspace_id={workspace_id}, config_id={config_id}")
api_logger.info(f"Async write service: workspace_id={workspace_id}, config_id={config_id}, language_type={language}")
# 获取 storage_type如果为 None 则使用默认值
storage_type = workspace_service.get_workspace_storage_type(
@@ -228,7 +240,7 @@ async def write_server_async(
task = celery_app.send_task(
"app.core.memory.agent.write_message",
args=[user_input.end_user_id, messages_list, config_id, storage_type, user_rag_memory_id]
args=[user_input.end_user_id, messages_list, config_id, storage_type, user_rag_memory_id, language]
)
api_logger.info(f"Write task queued: {task.id}")
@@ -653,7 +665,6 @@ async def get_knowledge_type_stats_api(
@router.get("/analytics/hot_memory_tags/by_user", response_model=ApiResponse)
async def get_hot_memory_tags_by_user_api(
end_user_id: Optional[str] = Query(None, description="用户ID可选"),
language_type: str = Header(default="zh", alias="X-Language-Type"),
limit: int = Query(20, description="返回标签数量限制"),
current_user: User = Depends(get_current_user),
db: Session=Depends(get_db),
@@ -661,28 +672,18 @@ async def get_hot_memory_tags_by_user_api(
"""
获取指定用户的热门记忆标签
注意:标签语言由写入时的 X-Language-Type 决定,查询时不进行翻译
返回格式:
[
{"name": "标签名", "frequency": 频次},
...
]
"""
workspace_id=current_user.current_workspace_id
workspace_repo = WorkspaceRepository(db)
workspace_models = workspace_repo.get_workspace_models_configs(workspace_id)
if workspace_models:
model_id = workspace_models.get("llm", None)
else:
model_id = None
api_logger.info(f"Hot memory tags by user requested: end_user_id={end_user_id}")
try:
result = await memory_agent_service.get_hot_memory_tags_by_user(
end_user_id=end_user_id,
language_type=language_type,
model_id=model_id,
limit=limit
)
return success(data=result, msg="获取热门记忆标签成功")

View File

@@ -3,6 +3,7 @@ import time
import uuid
from uuid import UUID
from app.core.language_utils import get_language_from_header
from app.core.logging_config import get_api_logger
from app.core.memory.storage_services.reflection_engine.self_reflexion import (
ReflectionConfig,
@@ -211,11 +212,13 @@ async def start_reflection_configs(
@router.get("/reflection/run")
async def reflection_run(
config_id: UUID|int,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> dict:
"""Activate the reflection function for all matching applications in the workspace"""
# 使用集中化的语言校验
language = get_language_from_header(language_type)
api_logger.info(f"用户 {current_user.username} 查询反思配置config_id: {config_id}")
config_id = resolve_config_id(config_id, db)

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status,Header
from app.core.language_utils import get_language_from_header
from app.core.logging_config import get_api_logger
from app.core.response_utils import success
from app.db import get_db
@@ -20,10 +21,13 @@ router = APIRouter(
@router.get("/short_term")
async def short_term_configs(
end_user_id: str,
language_type:str = Header(default="zh", alias="X-Language-Type"),
language_type:str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
# 使用集中化的语言校验
language = get_language_from_header(language_type)
# 获取短期记忆数据
short_term=ShortService(end_user_id)
short_result=short_term.get_short_databasets()

View File

@@ -6,6 +6,7 @@ from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.core.error_codes import BizCode
from app.core.language_utils import get_language_from_header
from app.core.logging_config import get_api_logger
from app.core.response_utils import fail, success
from app.db import get_db
@@ -33,6 +34,10 @@ from app.services.memory_storage_service import (
search_entity,
search_statement,
)
from fastapi import APIRouter, Depends, Header
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.utils.config_utils import resolve_config_id
# Get API logger
@@ -271,17 +276,21 @@ def read_all_config(
@router.post("/pilot_run", response_model=None)
async def pilot_run(
payload: ConfigPilotRun,
language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> StreamingResponse:
# 使用集中化的语言校验
language = get_language_from_header(language_type)
api_logger.info(
f"Pilot run requested: config_id={payload.config_id}, "
f"dialogue_text_length={len(payload.dialogue_text)}"
f"dialogue_text_length={len(payload.dialogue_text)}, language={language}"
)
payload.config_id = resolve_config_id(payload.config_id, db)
svc = DataConfigService(db)
return StreamingResponse(
svc.pilot_run_stream(payload),
svc.pilot_run_stream(payload, language=language),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",

View File

@@ -25,12 +25,12 @@ from fastapi import APIRouter, Depends, HTTPException, Header
from sqlalchemy.orm import Session
from app.core.error_codes import BizCode
from app.core.language_utils import get_language_from_header
from app.core.logging_config import get_api_logger
from app.core.response_utils import fail, success
from app.db import get_db
from app.dependencies import get_current_user
from app.models.user_model import User
from app.services.memory_base_service import Translation_English
from app.core.memory.models.ontology_models import OntologyClass
from typing import List
from app.schemas.ontology_schemas import (
@@ -63,72 +63,6 @@ router = APIRouter(
)
async def translate_ontology_classes(
classes: List[OntologyClass],
model_id: str
) -> List[OntologyClass]:
"""翻译本体类列表
将本体类的中文字段翻译为英文,包括:
- name_chinese: 中文名称
- description: 描述
- examples: 示例列表
Args:
classes: 本体类列表
model_id: LLM模型ID用于翻译
Returns:
List[OntologyClass]: 翻译后的本体类列表
"""
translated_classes = []
for ontology_class in classes:
# 创建类的副本,避免修改原对象
translated_class = ontology_class.model_copy(deep=True)
# 翻译 name_chinese 字段
if translated_class.name_chinese:
try:
translated_class.name_chinese = await Translation_English(
model_id,
translated_class.name_chinese
)
except Exception as e:
logger.warning(f"Failed to translate name_chinese: {e}")
# 保留原文
# 翻译 description 字段
if translated_class.description:
try:
translated_class.description = await Translation_English(
model_id,
translated_class.description
)
except Exception as e:
logger.warning(f"Failed to translate description: {e}")
# 保留原文
# 翻译 examples 列表
if translated_class.examples:
translated_examples = []
for example in translated_class.examples:
try:
translated_example = await Translation_English(
model_id,
example
)
translated_examples.append(translated_example)
except Exception as e:
logger.warning(f"Failed to translate example: {e}")
translated_examples.append(example) # 保留原文
translated_class.examples = translated_examples
translated_classes.append(translated_class)
return translated_classes
def _get_ontology_service(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
@@ -243,7 +177,7 @@ def _get_ontology_service(
@router.post("/extract", response_model=ApiResponse)
async def extract_ontology(
request: ExtractionRequest,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
@@ -260,31 +194,6 @@ async def extract_ontology(
db: 数据库会话
current_user: 当前用户
Returns:
ApiResponse: 包含提取结果的响应
Response format:
{
"code": 200,
"msg": "本体提取成功",
"data": {
"classes": [
{
"id": "147d9db50b524a9e909e01a753d3acdd",
"name": "Patient",
"name_chinese": "患者",
"description": "在医疗机构中接受诊疗、护理或健康管理的个体",
"examples": ["糖尿病患者", "术后康复患者", "门诊初诊患者"],
"parent_class": null,
"entity_type": "Person",
"domain": "Healthcare"
},
...
],
"domain": "Healthcare",
"extracted_count": 7
}
}
"""
api_logger.info(
f"Ontology extraction requested by user {current_user.id}, "
@@ -296,6 +205,9 @@ async def extract_ontology(
)
try:
# 使用集中化的语言校验
language = get_language_from_header(language_type)
# 获取当前工作空间ID
workspace_id = current_user.current_workspace_id
if not workspace_id:
@@ -314,33 +226,11 @@ async def extract_ontology(
scenario=request.scenario,
domain=request.domain,
scene_id=request.scene_id,
workspace_id=workspace_id
workspace_id=workspace_id,
language=language
)
# ===== 新增:翻译逻辑 =====
# 如果需要英文,则翻译数据
if language_type != 'zh':
api_logger.info(f"Translating extraction result to English")
# 翻译 classes 列表
result.classes = await translate_ontology_classes(
result.classes,
request.llm_id
)
# 翻译 domain 字段
if result.domain:
try:
result.domain = await Translation_English(
request.llm_id,
result.domain
)
except Exception as e:
logger.warning(f"Failed to translate domain: {e}")
# 保留原文
# ===== 翻译逻辑结束 =====
# 构建响应
# 构建响应(语言已在提取时通过模板控制,无需二次翻译)
response = ExtractionResponse(
classes=result.classes,
domain=result.domain,

View File

@@ -438,7 +438,8 @@ async def chat(
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
workspace_id=workspace_id
workspace_id=workspace_id,
files=payload.files # 传递多模态文件
):
yield event
@@ -475,7 +476,8 @@ async def chat(
memory=payload.memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
workspace_id=workspace_id
workspace_id=workspace_id,
files=payload.files # 传递多模态文件
)
return success(data=conversation_schema.ChatResponse(**result).model_dump(mode="json"))
elif app_type == AppType.MULTI_AGENT:

View File

@@ -155,7 +155,8 @@ async def chat(
memory=memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
workspace_id=workspace_id
workspace_id=workspace_id,
files=payload.files # 传递多模态文件
):
yield event
@@ -180,7 +181,8 @@ async def chat(
memory=memory,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
workspace_id=workspace_id
workspace_id=workspace_id,
files=payload.files # 传递多模态文件
)
return success(data=conversation_schema.ChatResponse(**result).model_dump(mode="json"))
elif app_type == AppType.MULTI_AGENT:

View File

@@ -8,11 +8,11 @@ from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends,Header
from app.db import get_db
from app.core.language_utils import get_language_from_header
from app.core.logging_config import get_api_logger
from app.core.response_utils import success, fail
from app.core.error_codes import BizCode
from app.core.api_key_utils import timestamp_to_datetime
from app.services.memory_base_service import Translation_English
from app.services.user_memory_service import (
UserMemoryService,
analytics_memory_types,
@@ -45,7 +45,6 @@ router = APIRouter(
@router.get("/analytics/memory_insight/report", response_model=ApiResponse)
async def get_memory_insight_report_api(
end_user_id: str,
language_type: str = Header(default="zh", alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> dict:
@@ -55,18 +54,10 @@ async def get_memory_insight_report_api(
此接口仅查询数据库中已缓存的记忆洞察数据,不执行生成操作。
如需生成新的洞察报告,请使用专门的生成接口。
"""
workspace_id = current_user.current_workspace_id
workspace_repo = WorkspaceRepository(db)
workspace_models = workspace_repo.get_workspace_models_configs(workspace_id)
if workspace_models:
model_id = workspace_models.get("llm", None)
else:
model_id = None
api_logger.info(f"记忆洞察报告查询请求: end_user_id={end_user_id}, user={current_user.username}")
try:
# 调用服务层获取缓存数据
result = await user_memory_service.get_cached_memory_insight(db, end_user_id,model_id,language_type)
result = await user_memory_service.get_cached_memory_insight(db, end_user_id)
if result["is_cached"]:
api_logger.info(f"成功返回缓存的记忆洞察报告: end_user_id={end_user_id}")
@@ -82,7 +73,7 @@ async def get_memory_insight_report_api(
@router.get("/analytics/user_summary", response_model=ApiResponse)
async def get_user_summary_api(
end_user_id: str,
language_type: str = Header(default="zh", alias="X-Language-Type"),
language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> dict:
@@ -91,7 +82,14 @@ async def get_user_summary_api(
此接口仅查询数据库中已缓存的用户摘要数据,不执行生成操作。
如需生成新的用户摘要,请使用专门的生成接口。
语言控制:
- 使用 X-Language-Type Header 指定语言
- 如果未传 Header默认使用中文 (zh)
"""
# 使用集中化的语言校验
language = get_language_from_header(language_type)
workspace_id = current_user.current_workspace_id
workspace_repo = WorkspaceRepository(db)
workspace_models = workspace_repo.get_workspace_models_configs(workspace_id)
@@ -103,7 +101,7 @@ async def get_user_summary_api(
api_logger.info(f"用户摘要查询请求: end_user_id={end_user_id}, user={current_user.username}")
try:
# 调用服务层获取缓存数据
result = await user_memory_service.get_cached_user_summary(db, end_user_id,model_id,language_type)
result = await user_memory_service.get_cached_user_summary(db, end_user_id,model_id,language)
if result["is_cached"]:
api_logger.info(f"成功返回缓存的用户摘要: end_user_id={end_user_id}")
@@ -119,6 +117,7 @@ async def get_user_summary_api(
@router.post("/analytics/generate_cache", response_model=ApiResponse)
async def generate_cache_api(
request: GenerateCacheRequest,
language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> dict:
@@ -127,7 +126,14 @@ async def generate_cache_api(
- 如果提供 end_user_id只为该用户生成
- 如果不提供,为当前工作空间的所有用户生成
语言控制:
- 使用 X-Language-Type Header 指定语言 ("zh" 中文, "en" 英文)
- 如果未传 Header默认使用中文 (zh)
"""
# 使用集中化的语言校验
language = get_language_from_header(language_type)
workspace_id = current_user.current_workspace_id
# 检查用户是否已选择工作空间
@@ -139,7 +145,7 @@ async def generate_cache_api(
api_logger.info(
f"缓存生成请求: user={current_user.username}, workspace={workspace_id}, "
f"end_user_id={end_user_id if end_user_id else '全部用户'}"
f"end_user_id={end_user_id if end_user_id else '全部用户'}, language={language}"
)
try:
@@ -148,10 +154,10 @@ async def generate_cache_api(
api_logger.info(f"开始为单个用户生成缓存: end_user_id={end_user_id}")
# 生成记忆洞察
insight_result = await user_memory_service.generate_and_cache_insight(db, end_user_id, workspace_id)
insight_result = await user_memory_service.generate_and_cache_insight(db, end_user_id, workspace_id, language=language)
# 生成用户摘要
summary_result = await user_memory_service.generate_and_cache_summary(db, end_user_id, workspace_id)
summary_result = await user_memory_service.generate_and_cache_summary(db, end_user_id, workspace_id, language=language)
# 构建响应
result = {
@@ -185,7 +191,7 @@ async def generate_cache_api(
# 为整个工作空间生成
api_logger.info(f"开始为工作空间 {workspace_id} 批量生成缓存")
result = await user_memory_service.generate_cache_for_workspace(db, workspace_id)
result = await user_memory_service.generate_cache_for_workspace(db, workspace_id, language=language)
# 记录统计信息
api_logger.info(
@@ -385,10 +391,13 @@ async def update_end_user_profile(
return fail(BizCode.INTERNAL_ERROR, "用户信息更新失败", error_msg)
@router.get("/memory_space/timeline_memories", response_model=ApiResponse)
async def memory_space_timeline_of_shared_memories(id: str, label: str,language_type: str = Header(default="zh", alias="X-Language-Type"),
async def memory_space_timeline_of_shared_memories(id: str, label: str,language_type: str = Header(default=None, alias="X-Language-Type"),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
# 使用集中化的语言校验
language = get_language_from_header(language_type)
workspace_id=current_user.current_workspace_id
workspace_repo = WorkspaceRepository(db)
workspace_models = workspace_repo.get_workspace_models_configs(workspace_id)
@@ -398,7 +407,7 @@ async def memory_space_timeline_of_shared_memories(id: str, label: str,language_
else:
model_id = None
MemoryEntity = MemoryEntityService(id, label)
timeline_memories_result = await MemoryEntity.get_timeline_memories_server(model_id, language_type)
timeline_memories_result = await MemoryEntity.get_timeline_memories_server(model_id, language)
return success(data=timeline_memories_result, msg="共同记忆时间线")
@router.get("/memory_space/relationship_evolution", response_model=ApiResponse)

View File

@@ -46,7 +46,9 @@ class LangChainAgent:
max_tokens: int = 2000,
system_prompt: Optional[str] = None,
tools: Optional[Sequence[BaseTool]] = None,
streaming: bool = False
streaming: bool = False,
max_iterations: Optional[int] = None, # 最大迭代次数None 表示自动计算)
max_tool_consecutive_calls: int = 3 # 单个工具最大连续调用次数
):
"""初始化 LangChain Agent
@@ -59,13 +61,36 @@ class LangChainAgent:
max_tokens: 最大 token 数
system_prompt: 系统提示词
tools: 工具列表(可选,框架自动走 ReAct 循环)
streaming: 是否启用流式输出(默认 True
streaming: 是否启用流式输出
max_iterations: 最大迭代次数None 表示自动计算:基础 5 次 + 每个工具 2 次)
max_tool_consecutive_calls: 单个工具最大连续调用次数(默认 3 次)
"""
self.model_name = model_name
self.provider = provider
self.system_prompt = system_prompt or "你是一个专业的AI助手"
self.tools = tools or []
self.streaming = streaming
self.max_tool_consecutive_calls = max_tool_consecutive_calls
# 工具调用计数器:记录每个工具的连续调用次数
self.tool_call_counter: Dict[str, int] = {}
self.last_tool_called: Optional[str] = None
# 根据工具数量动态调整最大迭代次数
# 基础值 + 每个工具额外的调用机会
if max_iterations is None:
# 自动计算:基础 5 次 + 每个工具 2 次额外机会
self.max_iterations = 5 + len(self.tools) * 2
else:
self.max_iterations = max_iterations
self.system_prompt = system_prompt or "你是一个专业的AI助手"
logger.debug(
f"Agent 迭代次数配置: max_iterations={self.max_iterations}, "
f"tool_count={len(self.tools)}, "
f"max_tool_consecutive_calls={self.max_tool_consecutive_calls}, "
f"auto_calculated={max_iterations is None}"
)
# 创建 RedBearLLM支持多提供商
model_config = RedBearModelConfig(
@@ -89,11 +114,14 @@ class LangChainAgent:
if streaming and hasattr(self._underlying_llm, 'streaming'):
self._underlying_llm.streaming = True
# 包装工具以跟踪连续调用次数
wrapped_tools = self._wrap_tools_with_tracking(self.tools) if self.tools else None
# 使用 create_agent 创建 agent graphLangChain 1.x 标准方式)
# 无论是否有工具,都使用 agent 统一处理
self.agent = create_agent(
model=self.llm,
tools=self.tools if self.tools else None,
tools=wrapped_tools,
system_prompt=self.system_prompt
)
@@ -105,17 +133,91 @@ class LangChainAgent:
"has_api_base": bool(api_base),
"temperature": temperature,
"streaming": streaming,
"max_iterations": self.max_iterations,
"max_tool_consecutive_calls": self.max_tool_consecutive_calls,
"tool_count": len(self.tools),
"tool_names": [tool.name for tool in self.tools] if self.tools else [],
# "tool_count": len(self.tools)
}
)
def _wrap_tools_with_tracking(self, tools: Sequence[BaseTool]) -> List[BaseTool]:
"""包装工具以跟踪连续调用次数
Args:
tools: 原始工具列表
Returns:
List[BaseTool]: 包装后的工具列表
"""
from langchain_core.tools import StructuredTool
from functools import wraps
wrapped_tools = []
for original_tool in tools:
tool_name = original_tool.name
original_func = original_tool.func if hasattr(original_tool, 'func') else None
if not original_func:
# 如果无法获取原始函数,直接使用原工具
wrapped_tools.append(original_tool)
continue
# 创建包装函数
def make_wrapped_func(tool_name, original_func):
"""创建包装函数的工厂函数,避免闭包问题"""
@wraps(original_func)
def wrapped_func(*args, **kwargs):
"""包装后的工具函数,跟踪连续调用次数"""
# 检查是否是连续调用同一个工具
if self.last_tool_called == tool_name:
self.tool_call_counter[tool_name] = self.tool_call_counter.get(tool_name, 0) + 1
else:
# 切换到新工具,重置计数器
self.tool_call_counter[tool_name] = 1
self.last_tool_called = tool_name
current_count = self.tool_call_counter[tool_name]
logger.debug(
f"工具调用: {tool_name}, 连续调用次数: {current_count}/{self.max_tool_consecutive_calls}"
)
# 检查是否超过最大连续调用次数
if current_count > self.max_tool_consecutive_calls:
logger.warning(
f"工具 '{tool_name}' 连续调用次数已达上限 ({self.max_tool_consecutive_calls})"
f"返回提示信息"
)
return (
f"工具 '{tool_name}' 已连续调用 {self.max_tool_consecutive_calls} 次,"
f"未找到有效结果。请尝试其他方法或直接回答用户的问题。"
)
# 调用原始工具函数
return original_func(*args, **kwargs)
return wrapped_func
# 使用 StructuredTool 创建新工具
wrapped_tool = StructuredTool(
name=original_tool.name,
description=original_tool.description,
func=make_wrapped_func(tool_name, original_func),
args_schema=original_tool.args_schema if hasattr(original_tool, 'args_schema') else None
)
wrapped_tools.append(wrapped_tool)
return wrapped_tools
def _prepare_messages(
self,
message: str,
history: Optional[List[Dict[str, str]]] = None,
context: Optional[str] = None
context: Optional[str] = None,
files: Optional[List[Dict[str, Any]]] = None
) -> List[BaseMessage]:
"""准备消息列表
@@ -123,6 +225,7 @@ class LangChainAgent:
message: 用户消息
history: 历史消息列表
context: 上下文信息
files: 多模态文件内容列表(已处理)
Returns:
List[BaseMessage]: 消息列表
@@ -145,7 +248,47 @@ class LangChainAgent:
if context:
user_content = f"参考信息:\n{context}\n\n用户问题:\n{user_content}"
messages.append(HumanMessage(content=user_content))
# 构建用户消息(支持多模态)
if files and len(files) > 0:
content_parts = self._build_multimodal_content(user_content, files)
messages.append(HumanMessage(content=content_parts))
else:
# 纯文本消息
messages.append(HumanMessage(content=user_content))
return messages
def _build_multimodal_content(self, text: str, files: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
构建多模态消息内容
Args:
text: 文本内容
files: 文件列表(已由 MultimodalService 处理为对应 provider 的格式)
Returns:
List[Dict]: 消息内容列表
"""
# 根据 provider 使用不同的文本格式
if self.provider.lower() in ["bedrock", "anthropic"]:
# Anthropic/Bedrock: {"type": "text", "text": "..."}
content_parts = [{"type": "text", "text": text}]
else:
# 通义千问等: {"text": "..."}
content_parts = [{"text": text}]
# 添加文件内容
# MultimodalService 已经根据 provider 返回了正确格式,直接使用
content_parts.extend(files)
logger.debug(
f"构建多模态消息: provider={self.provider}, "
f"parts={len(content_parts)}, "
f"files={len(files)}"
)
return content_parts
return messages
async def term_memory_save(self,long_term_messages,actual_config_id,end_user_id,type):
@@ -242,7 +385,8 @@ class LangChainAgent:
config_id: Optional[str] = None, # 添加这个参数
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
memory_flag: Optional[bool] = True
memory_flag: Optional[bool] = True,
files: Optional[List[Dict[str, Any]]] = None # 新增:多模态文件
) -> Dict[str, Any]:
"""执行对话
@@ -277,8 +421,8 @@ class LangChainAgent:
logger.info(f'写入类型{storage_type,str(end_user_id), message, str(user_rag_memory_id)}')
print(f'写入类型{storage_type,str(end_user_id), message, str(user_rag_memory_id)}')
try:
# 准备消息列表
messages = self._prepare_messages(message, history, context)
# 准备消息列表(支持多模态)
messages = self._prepare_messages(message, history, context, files)
logger.debug(
"准备调用 LangChain Agent",
@@ -286,23 +430,81 @@ class LangChainAgent:
"has_context": bool(context),
"has_history": bool(history),
"has_tools": bool(self.tools),
"message_count": len(messages)
"has_files": bool(files),
"message_count": len(messages),
"max_iterations": self.max_iterations
}
)
# 统一使用 agent.invoke 调用
result = await self.agent.ainvoke({"messages": messages})
# 通过 recursion_limit 限制最大迭代次数,防止工具调用死循环
try:
result = await self.agent.ainvoke(
{"messages": messages},
config={"recursion_limit": self.max_iterations}
)
except RecursionError as e:
logger.warning(
f"Agent 达到最大迭代次数限制 ({self.max_iterations}),可能存在工具调用循环",
extra={"error": str(e)}
)
# 返回一个友好的错误提示
return {
"content": f"抱歉,我在处理您的请求时遇到了问题。已达到最大处理步骤限制({self.max_iterations}次)。请尝试简化您的问题或稍后再试。",
"model": self.model_name,
"elapsed_time": time.time() - start_time,
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
# 获取最后的 AI 消息
output_messages = result.get("messages", [])
content = ""
logger.debug(f"输出消息数量: {len(output_messages)}")
total_tokens = 0
for msg in reversed(output_messages):
if isinstance(msg, AIMessage):
content = msg.content
logger.debug(f"找到 AI 消息content 类型: {type(msg.content)}")
logger.debug(f"AI 消息内容: {msg.content}")
# 处理多模态响应content 可能是字符串或列表
if isinstance(msg.content, str):
content = msg.content
logger.debug(f"提取字符串内容,长度: {len(content)}")
elif isinstance(msg.content, list):
# 多模态响应:提取文本部分
logger.debug(f"多模态响应,列表长度: {len(msg.content)}")
text_parts = []
for item in msg.content:
logger.debug(f"处理项: {item}")
if isinstance(item, dict):
# 通义千问格式: {"text": "..."}
if "text" in item:
text = item.get("text", "")
text_parts.append(text)
logger.debug(f"提取文本: {text[:100]}...")
# OpenAI 格式: {"type": "text", "text": "..."}
elif item.get("type") == "text":
text = item.get("text", "")
text_parts.append(text)
logger.debug(f"提取文本: {text[:100]}...")
elif isinstance(item, str):
text_parts.append(item)
logger.debug(f"提取字符串: {item[:100]}...")
content = "".join(text_parts)
logger.debug(f"合并后内容长度: {len(content)}")
else:
content = str(msg.content)
logger.debug(f"转换为字符串: {content[:100]}...")
response_meta = msg.response_metadata if hasattr(msg, 'response_metadata') else None
total_tokens = response_meta.get("token_usage", {}).get("total_tokens", 0) if response_meta else 0
break
logger.info(f"最终提取的内容长度: {len(content)}")
elapsed_time = time.time() - start_time
if memory_flag:
@@ -345,7 +547,8 @@ class LangChainAgent:
config_id: Optional[str] = None,
storage_type:Optional[str] = None,
user_rag_memory_id:Optional[str] = None,
memory_flag: Optional[bool] = True
memory_flag: Optional[bool] = True,
files: Optional[List[Dict[str, Any]]] = None # 新增:多模态文件
) -> AsyncGenerator[str, None]:
"""执行流式对话
@@ -382,11 +585,11 @@ class LangChainAgent:
# 注意:不在这里写入用户消息,等 AI 回复后一起写入
try:
# 准备消息列表
messages = self._prepare_messages(message, history, context)
# 准备消息列表(支持多模态)
messages = self._prepare_messages(message, history, context, files)
logger.debug(
f"准备流式调用has_tools={bool(self.tools)}, message_count={len(messages)}"
f"准备流式调用has_tools={bool(self.tools)}, has_files={bool(files)}, message_count={len(messages)}"
)
chunk_count = 0
@@ -398,7 +601,8 @@ class LangChainAgent:
try:
async for event in self.agent.astream_events(
{"messages": messages},
version="v2"
version="v2",
config={"recursion_limit": self.max_iterations}
):
chunk_count += 1
kind = event.get("event")
@@ -407,20 +611,70 @@ class LangChainAgent:
if kind == "on_chat_model_stream":
# LLM 流式输出
chunk = event.get("data", {}).get("chunk")
full_content+=chunk.content
if chunk and hasattr(chunk, "content") and chunk.content:
yield chunk.content
yielded_content = True
if chunk and hasattr(chunk, "content"):
# 处理多模态响应content 可能是字符串或列表
chunk_content = chunk.content
if isinstance(chunk_content, str) and chunk_content:
full_content += chunk_content
yield chunk_content
yielded_content = True
elif isinstance(chunk_content, list):
# 多模态响应:提取文本部分
for item in chunk_content:
if isinstance(item, dict):
# 通义千问格式: {"text": "..."}
if "text" in item:
text = item.get("text", "")
if text:
full_content += text
yield text
yielded_content = True
# OpenAI 格式: {"type": "text", "text": "..."}
elif item.get("type") == "text":
text = item.get("text", "")
if text:
full_content += text
yield text
yielded_content = True
elif isinstance(item, str):
full_content += item
yield item
yielded_content = True
elif kind == "on_llm_stream":
# 另一种 LLM 流式事件
chunk = event.get("data", {}).get("chunk")
if chunk:
if hasattr(chunk, "content") and chunk.content:
full_content+=chunk.content
yield chunk.content
yielded_content = True
if hasattr(chunk, "content"):
chunk_content = chunk.content
if isinstance(chunk_content, str) and chunk_content:
full_content += chunk_content
yield chunk_content
yielded_content = True
elif isinstance(chunk_content, list):
# 多模态响应:提取文本部分
for item in chunk_content:
if isinstance(item, dict):
# 通义千问格式: {"text": "..."}
if "text" in item:
text = item.get("text", "")
if text:
full_content += text
yield text
yielded_content = True
# OpenAI 格式: {"type": "text", "text": "..."}
elif item.get("type") == "text":
text = item.get("text", "")
if text:
full_content += text
yield text
yielded_content = True
elif isinstance(item, str):
full_content += item
yield item
yielded_content = True
elif isinstance(chunk, str):
full_content += chunk
yield chunk
yielded_content = True

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""语言处理工具模块
本模块提供集中化的语言校验和处理功能,确保整个应用中语言参数的一致性。
Functions:
validate_language: 校验语言参数,确保其为有效值
get_language_from_header: 从请求头获取并校验语言参数
"""
from typing import Optional
from app.core.logging_config import get_logger
logger = get_logger(__name__)
# 支持的语言列表
SUPPORTED_LANGUAGES = {"zh", "en"}
# 默认回退语言
DEFAULT_LANGUAGE = "zh"
def validate_language(language: Optional[str]) -> str:
"""
校验语言参数,确保其为有效值。
Args:
language: 待校验的语言代码,可以是 None、"zh""en" 或其他值
Returns:
有效的语言代码("zh""en"
Examples:
>>> validate_language("zh")
'zh'
>>> validate_language("en")
'en'
>>> validate_language("EN") # 大小写不敏感
'en'
>>> validate_language(None) # None 回退到默认值
'zh'
>>> validate_language("fr") # 不支持的语言回退到默认值
'zh'
"""
if language is None:
return DEFAULT_LANGUAGE
# 标准化:转小写并去除空白
lang = str(language).lower().strip()
if lang in SUPPORTED_LANGUAGES:
return lang
logger.warning(
f"无效的语言参数 '{language}',已回退到默认值 '{DEFAULT_LANGUAGE}'"
f"支持的语言: {SUPPORTED_LANGUAGES}"
)
return DEFAULT_LANGUAGE
def get_language_from_header(language_type: Optional[str]) -> str:
"""
从请求头获取并校验语言参数。
这是一个便捷函数,用于在 controller 层统一处理 X-Language-Type Header。
Args:
language_type: 从 X-Language-Type Header 获取的语言值
Returns:
有效的语言代码("zh""en"
Examples:
>>> get_language_from_header(None) # Header 未传递
'zh'
>>> get_language_from_header("en")
'en'
>>> get_language_from_header("invalid") # 无效值回退
'zh'
"""
return validate_language(language_type)

View File

@@ -10,7 +10,7 @@ async def write_node(state: WriteState) -> WriteState:
Write data to the database/file system.
Args:
state: WriteState containing messages, end_user_id, and memory_config
state: WriteState containing messages, end_user_id, memory_config, and language
Returns:
dict: Contains 'write_result' with status and data fields
@@ -18,6 +18,7 @@ async def write_node(state: WriteState) -> WriteState:
messages = state.get('messages', [])
end_user_id = state.get('end_user_id', '')
memory_config = state.get('memory_config', '')
language = state.get('language', 'zh') # 默认中文
# Convert LangChain messages to structured format expected by write()
structured_messages = []
@@ -35,6 +36,7 @@ async def write_node(state: WriteState) -> WriteState:
messages=structured_messages,
end_user_id=end_user_id,
memory_config=memory_config,
language=language,
)
logger.info(f"Write completed successfully! Config: {memory_config.config_name}")

View File

@@ -18,6 +18,7 @@ class WriteState(TypedDict):
memory_config: object
write_result: dict
data: str
language: str # 语言类型 ("zh" 中文, "en" 英文)
class ReadState(TypedDict):
"""

View File

@@ -33,17 +33,17 @@ async def write(
memory_config: MemoryConfig,
messages: list,
ref_id: str = "wyl20251027",
language: str = "zh",
) -> None:
"""
Execute the complete knowledge extraction pipeline.
Args:
user_id: User identifier
apply_id: Application identifier
end_user_id: Group identifier
memory_config: MemoryConfig object containing all configuration
messages: Structured message list [{"role": "user", "content": "..."}, ...]
ref_id: Reference ID, defaults to "wyl20251027"
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
"""
# Extract config values
embedding_model_id = str(memory_config.embedding_model_id)
@@ -99,6 +99,7 @@ async def write(
connector=neo4j_connector,
config=pipeline_config,
embedding_id=embedding_model_id,
language=language,
)
# Run the complete extraction pipeline
@@ -147,7 +148,7 @@ async def write(
step_start = time.time()
try:
summaries = await memory_summary_generation(
chunked_dialogs, llm_client=llm_client, embedder_client=embedder_client
chunked_dialogs, llm_client=llm_client, embedder_client=embedder_client, language=language
)
try:

View File

@@ -95,6 +95,7 @@ class ExtractionOrchestrator:
config: Optional[ExtractionPipelineConfig] = None,
progress_callback: Optional[Callable[[str, str, Optional[Dict[str, Any]]], Awaitable[None]]] = None,
embedding_id: Optional[str] = None,
language: str = "zh",
):
"""
初始化流水线编排器
@@ -108,6 +109,7 @@ class ExtractionOrchestrator:
- 接受 (stage: str, message: str, data: Optional[Dict[str, Any]]) 并返回 Awaitable[None]
- 在管线关键点调用以报告进度和结果数据
embedding_id: 嵌入模型ID如果为 None 则从全局配置获取(向后兼容)
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
"""
self.llm_client = llm_client
self.embedder_client = embedder_client
@@ -116,6 +118,7 @@ class ExtractionOrchestrator:
self.is_pilot_run = False # 默认非试运行模式
self.progress_callback = progress_callback # 保存进度回调函数
self.embedding_id = embedding_id # 保存嵌入模型ID
self.language = language # 保存语言配置
# 保存去重消歧的详细记录(内存中的数据结构)
self.dedup_merge_records: List[Dict[str, Any]] = [] # 实体合并记录
@@ -127,7 +130,7 @@ class ExtractionOrchestrator:
llm_client=llm_client,
config=self.config.statement_extraction,
)
self.triplet_extractor = TripletExtractor(llm_client=llm_client)
self.triplet_extractor = TripletExtractor(llm_client=llm_client, language=language)
self.temporal_extractor = TemporalExtractor(llm_client=llm_client)
logger.info("ExtractionOrchestrator 初始化完成")

View File

@@ -10,38 +10,11 @@ from app.core.memory.models.base_response import RobustLLMResponse
from app.core.memory.models.graph_models import MemorySummaryNode
from app.core.memory.models.message_models import DialogData
from app.core.memory.utils.prompt.prompt_utils import render_memory_summary_prompt
from app.core.language_utils import validate_language # 使用集中化的语言校验
from pydantic import Field
logger = get_memory_logger(__name__)
# 支持的语言列表和默认回退值
SUPPORTED_LANGUAGES = {"zh", "en"}
FALLBACK_LANGUAGE = "en"
def validate_language(language: Optional[str]) -> str:
"""
校验语言参数,确保其为有效值。
Args:
language: 待校验的语言代码
Returns:
有效的语言代码("zh""en"
"""
if language is None:
return FALLBACK_LANGUAGE
lang = str(language).lower().strip()
if lang in SUPPORTED_LANGUAGES:
return lang
logger.warning(
f"无效的语言参数 '{language}',已回退到默认值 '{FALLBACK_LANGUAGE}'"
f"支持的语言: {SUPPORTED_LANGUAGES}"
)
return FALLBACK_LANGUAGE
class MemorySummaryResponse(RobustLLMResponse):
"""Structured response for summary generation per chunk.
@@ -60,7 +33,7 @@ class MemorySummaryResponse(RobustLLMResponse):
async def generate_title_and_type_for_summary(
content: str,
llm_client,
language: str = None
language: str = "zh"
) -> Tuple[str, str]:
"""
为MemorySummary生成标题和类型
@@ -70,17 +43,14 @@ async def generate_title_and_type_for_summary(
Args:
content: Summary的内容文本
llm_client: LLM客户端实例
language: 生成标题使用的语言 ("zh" 中文, "en" 英文)如果为None则从配置读取
language: 生成标题使用的语言 ("zh" 中文, "en" 英文)默认中文
Returns:
(标题, 类型)元组
"""
from app.core.memory.utils.prompt.prompt_utils import render_episodic_title_and_type_prompt
from app.core.config import settings
# 如果没有指定语言,从配置中读取,并校验有效性
if language is None:
language = settings.DEFAULT_LANGUAGE
# 验证语言参数
language = validate_language(language)
# 定义有效的类型集合
@@ -188,6 +158,7 @@ async def _process_chunk_summary(
chunk,
llm_client,
embedder: OpenAIEmbedderClient,
language: str = "zh",
) -> Optional[MemorySummaryNode]:
"""Process a single chunk to generate a memory summary node."""
# Skip empty chunks
@@ -195,9 +166,8 @@ async def _process_chunk_summary(
return None
try:
# 从配置中获取语言设置(只获取一次,复用),并校验有效性
from app.core.config import settings
language = validate_language(settings.DEFAULT_LANGUAGE)
# 验证语言参数
language = validate_language(language)
# Render prompt via Jinja2 for a single chunk
prompt_content = await render_memory_summary_prompt(
@@ -267,13 +237,21 @@ async def memory_summary_generation(
chunked_dialogs: List[DialogData],
llm_client,
embedder_client: OpenAIEmbedderClient,
language: str = "zh",
) -> List[MemorySummaryNode]:
"""Generate memory summaries per chunk, embed them, and return nodes."""
"""Generate memory summaries per chunk, embed them, and return nodes.
Args:
chunked_dialogs: 分块后的对话数据
llm_client: LLM客户端
embedder_client: 嵌入客户端
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
"""
# Collect all tasks for parallel processing
tasks = []
for dialog in chunked_dialogs:
for chunk in dialog.chunks:
tasks.append(_process_chunk_summary(dialog, chunk, llm_client, embedder_client))
tasks.append(_process_chunk_summary(dialog, chunk, llm_client, embedder_client, language=language))
# Process all chunks in parallel
results = await asyncio.gather(*tasks, return_exceptions=False)

View File

@@ -64,6 +64,7 @@ class OntologyExtractor:
llm_max_tokens: int = 2000,
max_description_length: int = 500,
timeout: Optional[float] = None,
language: str = "zh",
) -> OntologyExtractionResponse:
"""Extract ontology classes from a scenario description.
@@ -84,6 +85,7 @@ class OntologyExtractor:
llm_max_tokens: LLM max tokens parameter (default: 2000)
max_description_length: Maximum description length (default: 500)
timeout: Optional timeout in seconds for LLM call (default: None, no timeout)
language: Language for output ("zh" for Chinese, "en" for English)
Returns:
OntologyExtractionResponse containing validated ontology classes
@@ -134,6 +136,7 @@ class OntologyExtractor:
max_classes=max_classes,
llm_temperature=llm_temperature,
llm_max_tokens=llm_max_tokens,
language=language,
),
timeout=timeout
)
@@ -156,6 +159,7 @@ class OntologyExtractor:
max_classes=max_classes,
llm_temperature=llm_temperature,
llm_max_tokens=llm_max_tokens,
language=language,
)
llm_duration = time.time() - llm_start_time
@@ -260,6 +264,7 @@ class OntologyExtractor:
max_classes: int,
llm_temperature: float,
llm_max_tokens: int,
language: str = "zh",
) -> OntologyExtractionResponse:
"""Call LLM to extract ontology classes from scenario.
@@ -272,6 +277,7 @@ class OntologyExtractor:
max_classes: Maximum number of classes to extract
llm_temperature: LLM temperature parameter
llm_max_tokens: LLM max tokens parameter
language: Language for output ("zh" for Chinese, "en" for English)
Returns:
OntologyExtractionResponse from LLM
@@ -286,6 +292,7 @@ class OntologyExtractor:
domain=domain,
max_classes=max_classes,
json_schema=OntologyExtractionResponse.model_json_schema(),
language=language,
)
logger.debug(f"Rendered prompt length: {len(prompt_content)}")

View File

@@ -17,13 +17,15 @@ logger = get_memory_logger(__name__)
class TripletExtractor:
"""Extracts knowledge triplets and entities from statements using LLM"""
def __init__(self, llm_client: OpenAIClient):
def __init__(self, llm_client: OpenAIClient, language: str = "zh"):
"""Initialize the TripletExtractor with an LLM client
Args:
llm_client: OpenAIClient instance for processing
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
"""
self.llm_client = llm_client
self.language = language
def _get_language(self) -> str:
"""Get the configured language for entity descriptions
@@ -31,8 +33,7 @@ class TripletExtractor:
Returns:
Language code ("zh" or "en")
"""
from app.core.config import settings
return settings.DEFAULT_LANGUAGE
return self.language
async def _extract_triplets(self, statement: Statement, chunk_content: str) -> TripletExtractionResponse:
"""Process a single statement and return extracted triplets and entities"""

View File

@@ -283,7 +283,8 @@ async def render_emotion_extraction_prompt(
async def render_emotion_suggestions_prompt(
health_data: dict,
patterns: dict,
user_profile: dict
user_profile: dict,
language: str = "zh"
) -> str:
"""
Renders the emotion suggestions generation prompt using the generate_emotion_suggestions.jinja2 template.
@@ -292,6 +293,7 @@ async def render_emotion_suggestions_prompt(
health_data: 情绪健康数据
patterns: 情绪模式分析结果
user_profile: 用户画像数据
language: 输出语言 ("zh" 中文, "en" 英文)
Returns:
Rendered prompt content as string
@@ -310,7 +312,8 @@ async def render_emotion_suggestions_prompt(
health_data=health_data,
patterns=patterns,
user_profile=user_profile,
emotion_distribution_json=emotion_distribution_json
emotion_distribution_json=emotion_distribution_json,
language=language
)
# 记录渲染结果到提示日志
@@ -328,7 +331,8 @@ async def render_emotion_suggestions_prompt(
async def render_user_summary_prompt(
user_id: str,
entities: str,
statements: str
statements: str,
language: str = "zh"
) -> str:
"""
Renders the user summary prompt using the user_summary.jinja2 template.
@@ -337,6 +341,7 @@ async def render_user_summary_prompt(
user_id: User identifier
entities: Core entities with frequency information
statements: Representative statement samples
language: The language to use for summary generation ("zh" for Chinese, "en" for English)
Returns:
Rendered prompt content as string
@@ -345,7 +350,8 @@ async def render_user_summary_prompt(
rendered_prompt = template.render(
user_id=user_id,
entities=entities,
statements=statements
statements=statements,
language=language
)
# 记录渲染结果到提示日志
@@ -354,7 +360,8 @@ async def render_user_summary_prompt(
log_template_rendering('user_summary.jinja2', {
'user_id': user_id,
'entities_len': len(entities),
'statements_len': len(statements)
'statements_len': len(statements),
'language': language
})
return rendered_prompt
@@ -363,7 +370,8 @@ async def render_user_summary_prompt(
async def render_memory_insight_prompt(
domain_distribution: str = None,
active_periods: str = None,
social_connections: str = None
social_connections: str = None,
language: str = "zh"
) -> str:
"""
Renders the memory insight prompt using the memory_insight.jinja2 template.
@@ -372,6 +380,7 @@ async def render_memory_insight_prompt(
domain_distribution: 核心领域分布信息
active_periods: 活跃时段信息
social_connections: 社交关联信息
language: The language to use for report generation ("zh" for Chinese, "en" for English)
Returns:
Rendered prompt content as string
@@ -380,7 +389,8 @@ async def render_memory_insight_prompt(
rendered_prompt = template.render(
domain_distribution=domain_distribution,
active_periods=active_periods,
social_connections=social_connections
social_connections=social_connections,
language=language
)
# 记录渲染结果到提示日志
@@ -389,7 +399,8 @@ async def render_memory_insight_prompt(
log_template_rendering('memory_insight.jinja2', {
'has_domain_distribution': bool(domain_distribution),
'has_active_periods': bool(active_periods),
'has_social_connections': bool(social_connections)
'has_social_connections': bool(social_connections),
'language': language
})
return rendered_prompt
@@ -424,7 +435,8 @@ async def render_ontology_extraction_prompt(
scenario: str,
domain: str | None = None,
max_classes: int = 15,
json_schema: dict | None = None
json_schema: dict | None = None,
language: str = "zh"
) -> str:
"""
Renders the ontology extraction prompt using the extract_ontology.jinja2 template.
@@ -434,6 +446,7 @@ async def render_ontology_extraction_prompt(
domain: Optional domain hint for the scenario (e.g., "Healthcare", "Education")
max_classes: Maximum number of classes to extract (default: 15)
json_schema: JSON schema for the expected output format
language: Language for output ("zh" for Chinese, "en" for English)
Returns:
Rendered prompt content as string
@@ -443,7 +456,8 @@ async def render_ontology_extraction_prompt(
scenario=scenario,
domain=domain,
max_classes=max_classes,
json_schema=json_schema
json_schema=json_schema,
language=language
)
# 记录渲染结果到提示日志

View File

@@ -1,19 +1,83 @@
===Task===
{% if language == "zh" %}
从给定的场景描述中提取本体类,遵循本体工程标准。
{% else %}
Extract ontology classes from the given scenario description following ontology engineering standards.
{% endif %}
===Role===
{% if language == "zh" %}
你是一位专业的本体工程师精通知识表示和OWLWeb本体语言标准。你的任务是从场景描述中识别抽象类和概念而不是具体实例。
{% else %}
You are a professional ontology engineer with expertise in knowledge representation and OWL (Web Ontology Language) standards. Your task is to identify abstract classes and concepts from scenario descriptions, not concrete instances.
{% endif %}
===Scenario Description===
{{ scenario }}
{% if domain -%}
===Domain Hint===
{% if language == "zh" %}
此场景属于 **{{ domain }}** 领域。提取类时请考虑领域特定的概念和术语。
{% else %}
This scenario belongs to the **{{ domain }}** domain. Consider domain-specific concepts and terminology when extracting classes.
{% endif %}
{%- endif %}
===Extraction Rules===
{% if language == "zh" %}
**1. 抽象类,而非实例:**
- 提取抽象类别和概念(如"医疗程序"、"患者"、"诊断"
- 不要提取具体实例(如"张三"、"301房间"、"2024-01-15"
- 以"事物的类型"而非"具体事物"的角度思考
**2. 命名规范:**
- "name"字段使用中文名称
- 使用清晰、描述性的中文名称
- 示例:"医疗程序"、"医疗服务提供者"、"诊断测试"
**3. 领域相关性:**
- 专注于场景领域的核心类
- 优先提取代表关键概念、实体或关系的类
- 避免过于通用的类(如"事物"、"对象"),除非它们在领域中有特定含义
**4. 类数量:**
- 提取5到{{ max_classes }}个类
- 目标是覆盖场景主要概念的平衡集合
- 质量优于数量:优先选择定义明确的类
**5. 清晰的描述:**
- 用中文提供简洁、信息丰富的描述最多500字
- 描述类代表什么,而不是具体实例
- 使用清晰、自然的中文解释类在领域中的作用
**6. 具体示例:**
- 为每个类提供2-5个中文具体实例示例
- 示例应该是该类的具体、现实的实例
- 示例有助于阐明类的范围和含义
- 示例格式:["示例1", "示例2", "示例3"]
**7. 类层次结构:**
- 在适用的情况下识别父子关系
- 使用parent_class字段指定继承关系
- 父类必须是提取的类之一或标准OWL类
- 顶级类的parent_class设为null
**8. 实体类型:**
- 为每个类分配适当的entity_type
- 常见类型:"人物"、"组织"、"地点"、"事件"、"概念"、"过程"、"对象"、"角色"
- 选择最具体的适用类型
**9. 语言一致性:**
- 所有字段内容必须使用中文
- "name"字段使用中文名称
- "description"字段使用中文描述
- "examples"字段使用中文示例
- "entity_type"字段使用中文类型名称
- "domain"字段使用中文领域名称
{% else %}
**1. Abstract Classes, Not Instances:**
- Extract abstract categories and concepts (e.g., "MedicalProcedure", "Patient", "Diagnosis")
- Do NOT extract concrete instances (e.g., "John Smith", "Room 301", "2024-01-15")
@@ -24,8 +88,6 @@ This scenario belongs to the **{{ domain }}** domain. Consider domain-specific c
- Examples: "MedicalProcedure", "HealthcareProvider", "DiagnosticTest"
- Avoid: "medical procedure", "healthcare_provider", "diagnostic-test"
- Use clear, descriptive names in English
- Avoid abbreviations unless they are standard in the domain (e.g., "API", "DNA")
- Provide Chinese translation in the "name_chinese" field (e.g., "医疗程序", "医疗服务提供者", "诊断测试")
**3. Domain Relevance:**
- Focus on classes that are central to the scenario's domain
@@ -38,16 +100,15 @@ This scenario belongs to the **{{ domain }}** domain. Consider domain-specific c
- Quality over quantity: prefer well-defined classes over exhaustive lists
**5. Clear Descriptions:**
- Provide concise, informative descriptions in Chinese (max 500 characters)
- Provide concise, informative descriptions in English (max 500 characters)
- Describe what the class represents, not specific instances
- Use clear, natural Chinese language that explains the class's role in the domain
- Use clear, natural English language
**6. Concrete Examples:**
- Provide 2-5 concrete instance examples in Chinese for each class
- Provide 2-5 concrete instance examples in English for each class
- Examples should be specific, realistic instances of the class
- Examples help clarify the class's scope and meaning
- Use natural Chinese language for examples
- Example format: ["示例1", "示例2", "示例3"]
- Example format: ["Example1", "Example2", "Example3"]
**7. Class Hierarchy:**
- Identify parent-child relationships where applicable
@@ -60,20 +121,119 @@ This scenario belongs to the **{{ domain }}** domain. Consider domain-specific c
- Common types: "Person", "Organization", "Location", "Event", "Concept", "Process", "Object", "Role"
- Choose the most specific type that applies
**9. OWL Reserved Words:**
- Do NOT use OWL reserved words as class names
- Reserved words include: "Thing", "Nothing", "Class", "Property", "ObjectProperty", "DatatypeProperty", "AnnotationProperty", "Ontology", "Individual", "Literal"
- If a reserved word is needed, add a domain-specific prefix (e.g., "MedicalClass" instead of "Class")
**10. Language Consistency:**
- Extract all class names in English (PascalCase format) for the "name" field
- Provide Chinese translation for class names in the "name_chinese" field
- Descriptions MUST be in Chinese (中文)
- Examples MUST be in Chinese (中文)
- Use clear, natural Chinese language for descriptions and examples
**9. Language Consistency:**
- All field content must be in English
- "name" field uses English PascalCase names
- "description" field uses English descriptions
- "examples" field uses English examples
- "entity_type" field uses English type names
- "domain" field uses English domain names
{% endif %}
===Examples===
{% if language == "zh" %}
**示例1医疗领域**
场景:"一家医院管理患者记录,安排预约,并协调医疗程序。医生诊断病情并开具治疗方案。"
输出:
{
"classes": [
{
"name": "患者",
"description": "在医疗机构接受医疗护理或治疗的人",
"examples": ["张三", "李四", "患有糖尿病的老年患者"],
"parent_class": null,
"entity_type": "人物",
"domain": "医疗"
},
{
"name": "医疗程序",
"description": "为医疗诊断或治疗而执行的系统性操作流程",
"examples": ["手术", "血液检查", "X光检查", "疫苗接种"],
"parent_class": null,
"entity_type": "过程",
"domain": "医疗"
},
{
"name": "诊断",
"description": "基于症状和检查结果对疾病或状况的识别",
"examples": ["糖尿病诊断", "癌症诊断", "流感诊断"],
"parent_class": null,
"entity_type": "概念",
"domain": "医疗"
},
{
"name": "医生",
"description": "诊断和治疗患者的持证医疗专业人员",
"examples": ["全科医生", "外科医生", "心脏病专家"],
"parent_class": null,
"entity_type": "角色",
"domain": "医疗"
},
{
"name": "治疗",
"description": "为治愈或管理疾病状况而提供的医疗护理或疗法",
"examples": ["药物治疗", "物理治疗", "化疗", "手术治疗"],
"parent_class": null,
"entity_type": "过程",
"domain": "医疗"
}
],
"domain": "医疗"
}
**示例2教育领域**
场景:"一所大学提供由教授教授的课程。学生注册项目,参加讲座,并完成作业以获得学位。"
输出:
{
"classes": [
{
"name": "学生",
"description": "在教育机构注册学习的人",
"examples": ["本科生", "研究生", "在职学生"],
"parent_class": null,
"entity_type": "角色",
"domain": "教育"
},
{
"name": "课程",
"description": "涵盖特定学科或主题的结构化教育课程",
"examples": ["计算机科学导论", "微积分I", "世界历史"],
"parent_class": null,
"entity_type": "概念",
"domain": "教育"
},
{
"name": "教授",
"description": "教授课程并进行研究的学术教师",
"examples": ["助理教授", "副教授", "正教授"],
"parent_class": null,
"entity_type": "角色",
"domain": "教育"
},
{
"name": "学术项目",
"description": "通向学位或证书的结构化课程体系",
"examples": ["理学学士", "文学硕士", "博士项目"],
"parent_class": null,
"entity_type": "概念",
"domain": "教育"
},
{
"name": "作业",
"description": "分配给学生以评估学习成果的任务或项目",
"examples": ["论文", "习题集", "研究报告", "实验报告"],
"parent_class": null,
"entity_type": "对象",
"domain": "教育"
}
],
"domain": "教育"
}
{% else %}
**Example 1 (Healthcare Domain):**
Scenario: "A hospital manages patient records, schedules appointments, and coordinates medical procedures. Doctors diagnose conditions and prescribe treatments."
@@ -82,52 +242,46 @@ Output:
"classes": [
{
"name": "Patient",
"name_chinese": "患者",
"description": "在医疗机构接受医疗护理或治疗的人",
"examples": ["张三", "李四", "患有糖尿病的老年患者"],
"description": "A person receiving medical care or treatment at a healthcare facility",
"examples": ["John Smith", "Jane Doe", "Elderly patient with diabetes"],
"parent_class": null,
"entity_type": "Person",
"domain": "Healthcare"
},
{
"name": "MedicalProcedure",
"name_chinese": "医疗程序",
"description": "为医疗诊断或治疗而执行的系统性操作流程",
"examples": ["手术", "血液检查", "X光检查", "疫苗接种"],
"description": "A systematic operation performed for medical diagnosis or treatment",
"examples": ["Surgery", "Blood test", "X-ray examination", "Vaccination"],
"parent_class": null,
"entity_type": "Process",
"domain": "Healthcare"
},
{
"name": "Diagnosis",
"name_chinese": "诊断",
"description": "基于症状和检查结果对疾病或状况的识别",
"examples": ["糖尿病诊断", "癌症诊断", "流感诊断"],
"description": "Identification of a disease or condition based on symptoms and examination results",
"examples": ["Diabetes diagnosis", "Cancer diagnosis", "Flu diagnosis"],
"parent_class": null,
"entity_type": "Concept",
"domain": "Healthcare"
},
{
"name": "Doctor",
"name_chinese": "医生",
"description": "诊断和治疗患者的持证医疗专业人员",
"examples": ["全科医生", "外科医生", "心脏病专家"],
"description": "A licensed medical professional who diagnoses and treats patients",
"examples": ["General practitioner", "Surgeon", "Cardiologist"],
"parent_class": null,
"entity_type": "Role",
"domain": "Healthcare"
},
{
"name": "Treatment",
"name_chinese": "治疗",
"description": "为治愈或管理疾病状况而提供的医疗护理或疗法",
"examples": ["药物治疗", "物理治疗", "化疗", "手术治疗"],
"description": "Medical care or therapy provided to cure or manage a disease condition",
"examples": ["Medication therapy", "Physical therapy", "Chemotherapy", "Surgical treatment"],
"parent_class": null,
"entity_type": "Process",
"domain": "Healthcare"
}
],
"domain": "Healthcare",
"namespace": "http://example.org/healthcare#"
"domain": "Healthcare"
}
**Example 2 (Education Domain):**
@@ -138,62 +292,48 @@ Output:
"classes": [
{
"name": "Student",
"name_chinese": "学生",
"description": "在教育机构注册学习的人",
"examples": ["本科生", "研究生", "在职学生"],
"description": "A person enrolled in an educational institution for learning",
"examples": ["Undergraduate student", "Graduate student", "Part-time student"],
"parent_class": null,
"entity_type": "Role",
"domain": "Education"
},
{
"name": "Course",
"name_chinese": "课程",
"description": "涵盖特定学科或主题的结构化教育课程",
"examples": ["计算机科学导论", "微积分I", "世界历史"],
"description": "A structured educational program covering a specific subject or topic",
"examples": ["Introduction to Computer Science", "Calculus I", "World History"],
"parent_class": null,
"entity_type": "Concept",
"domain": "Education"
},
{
"name": "Professor",
"name_chinese": "教授",
"description": "教授课程并进行研究的学术教师",
"examples": ["助理教授", "副教授", "正教授"],
"description": "An academic teacher who teaches courses and conducts research",
"examples": ["Assistant professor", "Associate professor", "Full professor"],
"parent_class": null,
"entity_type": "Role",
"domain": "Education"
},
{
"name": "AcademicProgram",
"name_chinese": "学术项目",
"description": "通向学位或证书的结构化课程体系",
"examples": ["理学学士", "文学硕士", "博士项目"],
"description": "A structured curriculum leading to a degree or certificate",
"examples": ["Bachelor of Science", "Master of Arts", "PhD program"],
"parent_class": null,
"entity_type": "Concept",
"domain": "Education"
},
{
"name": "Assignment",
"name_chinese": "作业",
"description": "分配给学生以评估学习成果的任务或项目",
"examples": ["论文", "习题集", "研究报告", "实验报告"],
"description": "A task or project assigned to students to assess learning outcomes",
"examples": ["Essay", "Problem set", "Research paper", "Lab report"],
"parent_class": null,
"entity_type": "Object",
"domain": "Education"
},
{
"name": "Lecture",
"name_chinese": "讲座",
"description": "由教师进行的教育性演讲或讲座",
"examples": ["入门讲座", "客座讲座", "在线讲座"],
"parent_class": null,
"entity_type": "Event",
"domain": "Education"
}
],
"domain": "Education",
"namespace": "http://example.org/education#"
"domain": "Education"
}
{% endif %}
===Output Format===
@@ -203,8 +343,12 @@ Output:
- Escape quotation marks in text with backslashes (\")
- Ensure proper string closure and comma separation
- No line breaks within JSON string values
- All class names must be in PascalCase format
- All class names must be unique (case-insensitive)
- Extract between 5 and {{ max_classes }} classes
{% if language == "zh" %}
- 所有字段内容必须使用中文
{% else %}
- All field content must be in English
{% endif %}
{{ json_schema }}

View File

@@ -6,9 +6,9 @@
Extract entities and knowledge triplets from the given statement.
{% if language == "zh" %}
**重要请使用中文生成实体描述description和示例example。**
**重要:请使用中文生成实体名称name描述description和示例example。**
{% else %}
**Important: Please generate entity descriptions and examples in English.**
**Important: Please generate entity names, descriptions and examples in English. If the original text is in Chinese, translate entity names to English.**
{% endif %}
===Inputs===
@@ -20,50 +20,48 @@ Extract entities and knowledge triplets from the given statement.
**Entity Extraction:**
- Extract entities with their types, context-independent descriptions, **concise examples**, aliases, and semantic memory classification
{% if language == "zh" %}
- **实体名称name必须使用中文**
- **实体描述description必须使用中文**
- **示例example必须使用中文**
{% else %}
- **Entity names must be in English** (translate if the original is in another language)
- **Entity descriptions must be in English**
- **Examples must be in English**
{% endif %}
- **Semantic Memory Classification (is_explicit_memory):**
* Set to `true` if the entity represents **explicit/semantic memory**:
- **Concepts:** "Machine Learning", "Photosynthesis", "Democracy", "人工智能", "光合作用", "民主"
- **Knowledge:** "Python Programming Language", "Theory of Relativity", "Python编程语言", "相对论"
- **Definitions:** "API (Application Programming Interface)", "REST API", "应用程序接口"
- **Principles:** "SOLID Principles", "First Law of Thermodynamics", "SOLID原则", "热力学第一定律"
- **Theories:** "Evolution Theory", "Quantum Mechanics", "进化论", "量子力学"
- **Methods/Techniques:** "Agile Development", "Machine Learning Algorithm", "敏捷开发", "机器学习算法"
- **Technical Terms:** "Neural Network", "Database", "神经网络", "数据库"
- **Concepts:** "Machine Learning", "Photosynthesis", "Democracy"
- **Knowledge:** "Python Programming Language", "Theory of Relativity"
- **Definitions:** "API (Application Programming Interface)", "REST API"
- **Principles:** "SOLID Principles", "First Law of Thermodynamics"
- **Theories:** "Evolution Theory", "Quantum Mechanics"
- **Methods/Techniques:** "Agile Development", "Machine Learning Algorithm"
- **Technical Terms:** "Neural Network", "Database"
* Set to `false` for:
- **People:** "John Smith", "Dr. Wang", "张明", "王博士"
- **Organizations:** "Microsoft", "Harvard University", "微软", "哈佛大学"
- **Locations:** "Beijing", "Central Park", "北京", "中央公园"
- **Events:** "2024 Conference", "Project Meeting", "2024会议", "项目会议"
- **Specific objects:** "iPhone 15", "Building A", "iPhone 15", "A栋"
- **People:** "John Smith", "Dr. Wang"
- **Organizations:** "Microsoft", "Harvard University"
- **Locations:** "Beijing", "Central Park"
- **Events:** "2024 Conference", "Project Meeting"
- **Specific objects:** "iPhone 15", "Building A"
- **Example Generation (IMPORTANT for semantic memory entities):**
* For entities where `is_explicit_memory=true`, generate a **concise example (around 20 characters)** to help understand the concept
* The example should be:
- **Specific and concrete**: Use real-world scenarios or applications
- **Brief**: Around 20 characters (can be slightly longer if needed for clarity)
- **In the same language as the entity name**
* Examples:
- Entity: "机器学习" → example: "如:用神经网络识别图片中的猫狗"
- Entity: "SOLID Principles" → example: "e.g., Single Responsibility, Open-Closed"
- Entity: "Photosynthesis" → example: "e.g., plants convert sunlight to energy"
- Entity: "人工智能" → example: "如:智能客服、自动驾驶"
{% if language == "zh" %}
- **使用中文**
{% else %}
- **In English**
{% endif %}
* For non-semantic entities (`is_explicit_memory=false`), the example field can be empty
- **Aliases Extraction (Important):**
* **CRITICAL: Extract aliases ONLY in the SAME LANGUAGE as the input text**
* **DO NOT translate or add aliases in different languages**
* Include common alternative names in the same language (e.g., "北京" → aliases: ["北平", "京城"])
* Include abbreviations and full names in the same language (e.g., "联合国" → aliases: ["联合国组织"])
* Include nicknames and common variations in the same language (e.g., "纽约" → aliases: ["纽约市", "大苹果"])
* If no aliases exist in the same language, use empty array: []
* **Examples:**
- Chinese input "北京" → aliases: ["北平", "京城"] (NOT ["Beijing", "Peking"])
- English input "Beijing" → aliases: ["Peking"] (NOT ["北京", "北平"])
- Chinese input "苹果公司" → aliases: ["苹果"] (NOT ["Apple Inc.", "Apple"])
- **Aliases Extraction:**
{% if language == "zh" %}
* 别名使用中文
{% else %}
* Aliases should be in English
{% endif %}
* Include common alternative names, abbreviations and full names
* If no aliases exist, use empty array: []
- Exclude lengthy quotes, calendar dates, temporal ranges, and temporal expressions
- For numeric values: extract as separate entities (instance_of: 'Numeric', name: units, numeric_value: value)
Example: £30 → name: 'GBP', numeric_value: 30, instance_of: 'Numeric'
@@ -73,6 +71,11 @@ Extract entities and knowledge triplets from the given statement.
- Subject: main entity performing the action or being described
- Predicate: relationship between entities (e.g., 'is', 'works at', 'believes')
- Object: entity, value, or concept affected by the predicate
{% if language == "zh" %}
- subject_name 和 object_name 必须使用中文
{% else %}
- subject_name and object_name must be in English (translate if original is in another language)
{% endif %}
- Exclude all temporal expressions from every field
- Use ONLY the predicates listed in "Predicate Instructions" (uppercase English tokens)
- Do NOT translate predicate tokens
@@ -81,7 +84,7 @@ Extract entities and knowledge triplets from the given statement.
**When NOT to extract triplets:**
- Non-propositional utterances (emotions, fillers, onomatopoeia)
- No clear predicate from the given definitions applies
- Standalone noun phrases or checklist items (e.g., "三脚架", "备用电池") → extract as entities only
- Standalone noun phrases or checklist items → extract as entities only
- Do NOT invent generic predicates (e.g., "IS_DOING", "FEELS", "MENTIONS")
**If no valid triplet exists:** Return triplets: [], extract entities if present, otherwise both arrays empty.
@@ -96,248 +99,82 @@ Use ONLY these predicates. If none fits, set triplets to [].
===Examples===
**Example 1 (English):** "I plan to travel to Paris next week and visit the Louvre."
{% if language == "en" %}
**Example 1 (English output):** "I plan to travel to Paris next week and visit the Louvre."
Output:
{
"triplets": [
{
"subject_name": "I",
"subject_id": 0,
"predicate": "PLANS_TO_VISIT",
"object_name": "Paris",
"object_id": 1,
"value": null
},
{
"subject_name": "I",
"subject_id": 0,
"predicate": "PLANS_TO_VISIT",
"object_name": "Louvre",
"object_id": 2,
"value": null
}
{"subject_name": "I", "subject_id": 0, "predicate": "PLANS_TO_VISIT", "object_name": "Paris", "object_id": 1, "value": null},
{"subject_name": "I", "subject_id": 0, "predicate": "PLANS_TO_VISIT", "object_name": "Louvre", "object_id": 2, "value": null}
],
"entities": [
{
"entity_idx": 0,
"name": "I",
"type": "Person",
"description": "The user",
"example": "",
"aliases": [],
"is_explicit_memory": false
},
{
"entity_idx": 1,
"name": "Paris",
"type": "Location",
"description": "Capital city of France",
"example": "",
"aliases": [],
"is_explicit_memory": false
},
{
"entity_idx": 2,
"name": "Louvre",
"type": "Location",
"description": "World-famous museum located in Paris",
"example": "",
"aliases": ["Louvre Museum"],
"is_explicit_memory": false
}
{"entity_idx": 0, "name": "I", "type": "Person", "description": "The user", "example": "", "aliases": [], "is_explicit_memory": false},
{"entity_idx": 1, "name": "Paris", "type": "Location", "description": "Capital city of France", "example": "", "aliases": [], "is_explicit_memory": false},
{"entity_idx": 2, "name": "Louvre", "type": "Location", "description": "World-famous museum located in Paris", "example": "", "aliases": ["Louvre Museum"], "is_explicit_memory": false}
]
}
**Example 2 (English):** "John Smith works at Google and is responsible for AI product development."
**Example 2 (Chinese input → English output - IMPORTANT: translate entity names):** "张明在腾讯工作负责AI产品开发。"
Output:
{
"triplets": [
{
"subject_name": "John Smith",
"subject_id": 0,
"predicate": "WORKS_AT",
"object_name": "Google",
"object_id": 1,
"value": null
},
{
"subject_name": "John Smith",
"subject_id": 0,
"predicate": "RESPONSIBLE_FOR",
"object_name": "AI product development",
"object_id": 2,
"value": null
}
{"subject_name": "Zhang Ming", "subject_id": 0, "predicate": "WORKS_AT", "object_name": "Tencent", "object_id": 1, "value": null},
{"subject_name": "Zhang Ming", "subject_id": 0, "predicate": "RESPONSIBLE_FOR", "object_name": "AI product development", "object_id": 2, "value": null}
],
"entities": [
{
"entity_idx": 0,
"name": "John Smith",
"type": "Person",
"description": "Individual person name",
"example": "",
"aliases": [],
"is_explicit_memory": false
},
{
"entity_idx": 1,
"name": "Google",
"type": "Organization",
"description": "American technology company",
"example": "",
"aliases": ["Google LLC", "Alphabet Inc."],
"is_explicit_memory": false
},
{
"entity_idx": 2,
"name": "AI product development",
"type": "Concept",
"description": "Artificial intelligence product development work",
"example": "e.g., developing chatbots, recommendation systems",
"aliases": [],
"is_explicit_memory": true
}
{"entity_idx": 0, "name": "Zhang Ming", "type": "Person", "description": "Individual person name", "example": "", "aliases": [], "is_explicit_memory": false},
{"entity_idx": 1, "name": "Tencent", "type": "Organization", "description": "Chinese technology company", "example": "", "aliases": ["Tencent Holdings"], "is_explicit_memory": false},
{"entity_idx": 2, "name": "AI product development", "type": "Concept", "description": "Artificial intelligence product development work", "example": "e.g., developing chatbots", "aliases": [], "is_explicit_memory": true}
]
}
**Example 3 (Chinese):** "我计划下周去巴黎旅行,参观卢浮宫。"
Output:
{
"triplets": [
{
"subject_name": "我",
"subject_id": 0,
"predicate": "PLANS_TO_VISIT",
"object_name": "巴黎",
"object_id": 1,
"value": null
},
{
"subject_name": "我",
"subject_id": 0,
"predicate": "PLANS_TO_VISIT",
"object_name": "卢浮宫",
"object_id": 2,
"value": null
}
],
"entities": [
{
"entity_idx": 0,
"name": "我",
"type": "Person",
"description": "用户本人",
"example": "",
"aliases": [],
"is_explicit_memory": false
},
{
"entity_idx": 1,
"name": "巴黎",
"type": "Location",
"description": "法国首都城市",
"example": "",
"aliases": [],
"is_explicit_memory": false
},
{
"entity_idx": 2,
"name": "卢浮宫",
"type": "Location",
"description": "位于巴黎的世界著名博物馆",
"example": "",
"aliases": [],
"is_explicit_memory": false
}
]
}
**Example 4 (Chinese):** "张明在腾讯工作负责AI产品开发。"
Output:
{
"triplets": [
{
"subject_name": "张明",
"subject_id": 0,
"predicate": "WORKS_AT",
"object_name": "腾讯",
"object_id": 1,
"value": null
},
{
"subject_name": "张明",
"subject_id": 0,
"predicate": "RESPONSIBLE_FOR",
"object_name": "AI产品开发",
"object_id": 2,
"value": null
}
],
"entities": [
{
"entity_idx": 0,
"name": "张明",
"type": "Person",
"description": "个人姓名",
"example": "",
"aliases": [],
"is_explicit_memory": false
},
{
"entity_idx": 1,
"name": "腾讯",
"type": "Organization",
"description": "中国科技公司",
"example": "",
"aliases": ["腾讯控股", "腾讯公司"],
"is_explicit_memory": false
},
{
"entity_idx": 2,
"name": "AI产品开发",
"type": "Concept",
"description": "人工智能产品研发工作",
"example": "如:开发智能客服机器人、推荐系统",
"aliases": [],
"is_explicit_memory": true
}
]
}
**Example 5 (Entity Only - English):** "Tripod"
**Example 3 (Chinese input → English output):** "三脚架"
Output:
{
"triplets": [],
"entities": [
{
"entity_idx": 0,
"name": "Tripod",
"type": "Equipment",
"description": "Photography equipment accessory",
"example": "",
"aliases": ["Camera Tripod"],
"is_explicit_memory": false
}
{"entity_idx": 0, "name": "Tripod", "type": "Equipment", "description": "Photography equipment accessory", "example": "", "aliases": ["Camera Tripod"], "is_explicit_memory": false}
]
}
{% else %}
**Example 1 (English input → Chinese output):** "I plan to travel to Paris next week and visit the Louvre."
Output:
{
"triplets": [
{"subject_name": "我", "subject_id": 0, "predicate": "PLANS_TO_VISIT", "object_name": "巴黎", "object_id": 1, "value": null},
{"subject_name": "我", "subject_id": 0, "predicate": "PLANS_TO_VISIT", "object_name": "卢浮宫", "object_id": 2, "value": null}
],
"entities": [
{"entity_idx": 0, "name": "我", "type": "Person", "description": "用户本人", "example": "", "aliases": [], "is_explicit_memory": false},
{"entity_idx": 1, "name": "巴黎", "type": "Location", "description": "法国首都城市", "example": "", "aliases": [], "is_explicit_memory": false},
{"entity_idx": 2, "name": "卢浮宫", "type": "Location", "description": "位于巴黎的世界著名博物馆", "example": "", "aliases": [], "is_explicit_memory": false}
]
}
**Example 6 (Entity Only - Chinese):** "三脚架"
**Example 2 (Chinese input → Chinese output):** "张明在腾讯工作负责AI产品开发。"
Output:
{
"triplets": [
{"subject_name": "张明", "subject_id": 0, "predicate": "WORKS_AT", "object_name": "腾讯", "object_id": 1, "value": null},
{"subject_name": "张明", "subject_id": 0, "predicate": "RESPONSIBLE_FOR", "object_name": "AI产品开发", "object_id": 2, "value": null}
],
"entities": [
{"entity_idx": 0, "name": "张明", "type": "Person", "description": "个人姓名", "example": "", "aliases": [], "is_explicit_memory": false},
{"entity_idx": 1, "name": "腾讯", "type": "Organization", "description": "中国科技公司", "example": "", "aliases": ["腾讯控股", "腾讯公司"], "is_explicit_memory": false},
{"entity_idx": 2, "name": "AI产品开发", "type": "Concept", "description": "人工智能产品研发工作", "example": "如:开发智能客服机器人", "aliases": [], "is_explicit_memory": true}
]
}
**Example 3 (Entity Only - Chinese):** "三脚架"
Output:
{
"triplets": [],
"entities": [
{
"entity_idx": 0,
"name": "三脚架",
"type": "Equipment",
"description": "摄影器材配件",
"example": "",
"aliases": ["相机三脚架"],
"is_explicit_memory": false
}
{"entity_idx": 0, "name": "三脚架", "type": "Equipment", "description": "摄影器材配件", "example": "", "aliases": ["相机三脚架"], "is_explicit_memory": false}
]
}
{% endif %}
===End of Examples===
===Output Format===
@@ -348,10 +185,10 @@ Output:
- Ensure proper string closure and comma separation
- No line breaks within JSON string values
{% if language == "zh" %}
- **语言要求实体描述description示例example必须使用中文**
- **语言要求:实体名称name描述description示例example、subject_name、object_name 必须使用中文**
{% else %}
- **Language Requirement: Entity descriptions and examples must be in English**
- **Language Requirement: Entity names, descriptions, examples, subject_name, object_name must be in English**
- **If the original text is in Chinese, translate all names to English**
{% endif %}
- Preserve the original language and do not translate
{{ json_schema }}
{{ json_schema }}

View File

@@ -1,3 +1,69 @@
{% if language == "en" %}
You are a professional mental health consultant. Based on the following user's emotional health data and personal information, generate 3-5 personalized emotional improvement suggestions.
## User Emotional Health Data
Health Score: {{ health_data.health_score }}/100
Health Level: {{ health_data.level }}
Dimension Analysis:
- Positivity Rate: {{ health_data.dimensions.positivity_rate.score }}/100
- Positive Emotions: {{ health_data.dimensions.positivity_rate.positive_count }} times
- Negative Emotions: {{ health_data.dimensions.positivity_rate.negative_count }} times
- Neutral Emotions: {{ health_data.dimensions.positivity_rate.neutral_count }} times
- Stability: {{ health_data.dimensions.stability.score }}/100
- Standard Deviation: {{ health_data.dimensions.stability.std_deviation }}
- Resilience: {{ health_data.dimensions.resilience.score }}/100
- Recovery Rate: {{ health_data.dimensions.resilience.recovery_rate }}
Emotion Distribution:
{{ emotion_distribution_json }}
## Emotion Pattern Analysis
Dominant Negative Emotion: {{ patterns.dominant_negative_emotion|default('None') }}
Emotion Volatility: {{ patterns.emotion_volatility|default('Unknown') }}
High Intensity Emotion Count: {{ patterns.high_intensity_emotions|default([])|length }}
## User Interests
{{ user_profile.interests|default(['Unknown'])|join(', ') }}
## Task Requirements
Please generate 3-5 personalized suggestions, each containing:
1. type: Suggestion type (Emotion Balance/Activity Recommendation/Social Connection/Stress Management)
2. title: Suggestion title (short and impactful)
3. content: Suggestion content (detailed explanation, 50-100 words)
4. priority: Priority level (High/Medium/Low)
5. actionable_steps: 3 specific executable steps
Also provide a health_summary (no more than 50 words) summarizing the user's overall emotional state.
Please return in JSON format as follows:
{
"health_summary": "Your emotional health status...",
"suggestions": [
{
"type": "Emotion Balance",
"title": "Suggestion Title",
"content": "Suggestion content...",
"priority": "High",
"actionable_steps": ["Step 1", "Step 2", "Step 3"]
}
]
}
Notes:
- Suggestions should be specific and actionable, avoid vague advice
- Provide personalized suggestions based on user's interests and hobbies
- Provide targeted suggestions for main issues (such as dominant negative emotions)
- Allocate priorities reasonably (at least 1 high, 1-2 medium, rest low)
- The 3 steps for each suggestion should be progressive and easy to implement
- All output must be in English
{% else %}
你是一位专业的心理健康顾问。请根据以下用户的情绪健康数据和个人信息生成3-5条个性化的情绪改善建议。
## 用户情绪健康数据
@@ -33,10 +99,10 @@
## 任务要求
请生成3-5条个性化建议每条建议包含
1. type: 建议类型(emotion_balance/activity_recommendation/social_connection/stress_management
1. type: 建议类型(情绪平衡/活动建议/社交联系/压力管理
2. title: 建议标题(简短有力)
3. content: 建议内容详细说明50-100字
4. priority: 优先级(high/medium/low
4. priority: 优先级(高/中/低
5. actionable_steps: 3个可执行的具体步骤
同时提供一个health_summary不超过50字概括用户的整体情绪状态。
@@ -46,10 +112,10 @@
"health_summary": "您的情绪健康状况...",
"suggestions": [
{
"type": "emotion_balance",
"type": "情绪平衡",
"title": "建议标题",
"content": "建议内容...",
"priority": "high",
"priority": "",
"actionable_steps": ["步骤1", "步骤2", "步骤3"]
}
]
@@ -61,3 +127,4 @@
- 针对主要问题(如主要负面情绪)提供针对性建议
- 优先级要合理分配至少1个high1-2个medium其余low
- 每个建议的3个步骤要循序渐进、易于实施
{% endif %}

View File

@@ -7,6 +7,12 @@
Your task is to generate a comprehensive memory insight report based on the provided data analysis. The report should include four distinct sections that capture different aspects of the user's memory patterns and characteristics.
{% if language == "zh" %}
**重要:请使用中文生成记忆洞察报告内容。**
{% else %}
**Important: Please generate the memory insight report content in English.**
{% endif %}
===Inputs===
{% if domain_distribution %}
@@ -31,56 +37,105 @@ Your task is to generate a comprehensive memory insight report based on the prov
**Section-Specific Requirements:**
1. **总体概述 (Overview)** (100-150 Chinese characters)
- Focus on: Overall analysis of user profile based on interaction logs
- Describe the user's main role, work network, and collaboration spirit
- Use professional, data-driven language style
- Example reference: "通过对156次交互日志的深度分析系统发现三层一位主要用户档案和数据分析的产品经理。他的工作网络体现出鲜明的目标导向和团队协作精神。"
{% if language == "zh" %}
1. **总体概述** (100-150字)
- 重点:基于交互日志对用户档案进行整体分析
- 描述用户的主要角色、工作网络和协作精神
- 使用专业、数据驱动的语言风格
- 示例参考:"通过对156次交互日志的深度分析系统发现张三是一位主要从事用户档案和数据分析的产品经理。他的工作网络体现出鲜明的目标导向和团队协作精神。"
2. **行为模式 (Behavior Pattern)** (80-120 Chinese characters)
- Focus on: Work patterns, time regularity, and behavioral characteristics
- Describe weekly work patterns and time preferences
- Use objective, analytical language
- Example reference: "张三的工作模式呈现出鲜明的周期性:周一通常用于规划和会议,周三周四专注于产品设计和用户研究,周五进行总结和复盘。他倾向于在上午进行头脑风暴,下午处理执行性工作。"
2. **行为模式** (80-120字)
- 重点:工作模式、时间规律和行为特征
- 描述每周工作模式和时间偏好
- 使用客观、分析性的语言
- 示例参考:"张三的工作模式呈现出鲜明的周期性:周一通常用于规划和会议,周三周四专注于产品设计和用户研究,周五进行总结和复盘。他倾向于在上午进行头脑风暴,下午处理执行性工作。"
3. **关键发现 (Key Findings)** (3-4 bullet points, 30-50 characters each)
- Focus on: Specific, insightful observations about user behavior and preferences
- Use bullet points (•) format
- Each finding should be concrete and data-supported
- Example reference:
3. **关键发现** (3-4个要点每个30-50字)
- 重点:关于用户行为和偏好的具体、有洞察力的观察
- 使用项目符号(•)格式
- 每个发现应具体且有数据支持
- 示例参考:
"• 在产品决策中张三总是优先考虑用户反应这在68%的决策记录中得到体现
• 他善于使用数据可视化工具来支持论点,这种习惯在项目管理中发挥了重要作用
• 团队成员对他的评价中,"思路清晰"和"思路敏捷"两个关键词出现频率最高
• 他对AI机器学习领域保持持续关注近3个月参加了7次相关培训"
4. **成长轨迹 (Growth Trajectory)** (100-150 Chinese characters)
4. **成长轨迹** (100-150字)
- 重点:用户的成长历程、关键里程碑和能力提升
- 按时间顺序组织内容
- 突出角色变化和成就
- 使用积极、鼓励的语气
- 示例参考:"从入职时的产品经理成长为高级产品经理,张三在产品规划、团队管理和技术理解三个方面都有显著提升。特别是在最近一年,他开始独立主导更复杂的项目,展现出更强的战略思维能力。他的成长轨迹显示出对新技术的持续学习和对产品思维的不断深化。"
{% else %}
1. **Overview** (100-150 words)
- Focus on: Overall analysis of user profile based on interaction logs
- Describe the user's main role, work network, and collaboration spirit
- Use professional, data-driven language style
- Example reference: "Through in-depth analysis of 156 interaction logs, the system identified Zhang San as a product manager primarily focused on user profiling and data analysis. His work network demonstrates a clear goal-oriented approach and team collaboration spirit."
2. **Behavior Pattern** (80-120 words)
- Focus on: Work patterns, time regularity, and behavioral characteristics
- Describe weekly work patterns and time preferences
- Use objective, analytical language
- Example reference: "Zhang San's work pattern shows distinct periodicity: Mondays are typically used for planning and meetings, Wednesdays and Thursdays focus on product design and user research, and Fridays are for summary and review. He tends to brainstorm in the morning and handle execution tasks in the afternoon."
3. **Key Findings** (3-4 bullet points, 30-50 words each)
- Focus on: Specific, insightful observations about user behavior and preferences
- Use bullet points (•) format
- Each finding should be concrete and data-supported
- Example reference:
"• In product decisions, Zhang San always prioritizes user feedback, as evidenced in 68% of decision records
• He excels at using data visualization tools to support arguments, a habit that plays an important role in project management
• Among team member evaluations, 'clear thinking' and 'quick thinking' are the most frequently mentioned keywords
• He maintains continuous attention to AI and machine learning, attending 7 related training sessions in the past 3 months"
4. **Growth Trajectory** (100-150 words)
- Focus on: User's growth journey, key milestones, and capability improvements
- Organize content chronologically
- Highlight role changes and achievements
- Use positive, encouraging tone
- Example reference: "从入职时的产品经理成长为高级产品经理,张三在产品单独、团队管理和技术理解三个方面都有显著提升。特别是在最近一年,他开始独立主导更复杂的项目,展现出更强的战略思维能力。他的成长轨迹显示出对新技术的持续学习和对产品思维的不断深化。"
- Example reference: "Growing from a product manager at entry to a senior product manager, Zhang San has shown significant improvement in product planning, team management, and technical understanding. Especially in the past year, he has begun to independently lead more complex projects, demonstrating stronger strategic thinking capabilities. His growth trajectory shows continuous learning of new technologies and deepening of product thinking."
{% endif %}
===Output Format (MUST STRICTLY FOLLOW)===
{% if language == "zh" %}
【总体概述】
[100-150 characters describing overall user profile and work network based on interaction analysis]
[100-150字,基于交互分析描述用户整体档案和工作网络]
【行为模式】
[80-120 characters describing work patterns, time regularity, and behavioral characteristics]
[80-120字,描述工作模式、时间规律和行为特征]
【关键发现】
• [First key finding with data support, 30-50 characters]
• [Second key finding with data support, 30-50 characters]
• [Third key finding with data support, 30-50 characters]
• [Fourth key finding with data support, 30-50 characters]
• [第一个关键发现有数据支持30-50字]
• [第二个关键发现有数据支持30-50字]
• [第三个关键发现有数据支持30-50字]
• [第四个关键发现有数据支持30-50字]
【成长轨迹】
[100-150 characters describing growth journey, milestones, and capability improvements]
[100-150字,描述成长历程、关键里程碑和能力提升]
{% else %}
【Overview】
[100-150 words describing overall user profile and work network based on interaction analysis]
【Behavior Pattern】
[80-120 words describing work patterns, time regularity, and behavioral characteristics]
【Key Findings】
• [First key finding with data support, 30-50 words]
• [Second key finding with data support, 30-50 words]
• [Third key finding with data support, 30-50 words]
• [Fourth key finding with data support, 30-50 words]
【Growth Trajectory】
[100-150 words describing growth journey, milestones, and capability improvements]
{% endif %}
===Example===
{% if language == "zh" %}
Example Input:
- 核心领域分布: 产品管理(38%), 数据分析(24%), 团队协作(21%)
- 活跃时段: 用户在每年的 4 和 10 月最为活跃
@@ -101,6 +156,28 @@ Example Output:
【成长轨迹】
从入职时的产品经理成长为高级产品经理张三在产品规划、团队管理和技术理解三个方面都有显著提升。特别是在最近一年他开始独立主导更复杂的项目展现出更强的战略思维能力。他与李明的47条共同记忆见证了他的成长历程。
{% else %}
Example Input:
- Core Domain Distribution: Product Management (38%), Data Analysis (24%), Team Collaboration (21%)
- Active Periods: User is most active in April and October each year
- Social Connections: Has the most shared memories (47 entries) with user "Li Ming", primarily during 2020-2023
Example Output:
【Overview】
Through in-depth analysis of 156 interaction logs, the system identified Zhang San as a product manager primarily focused on user profiling and data analysis. His work network demonstrates a clear goal-oriented approach and team collaboration spirit, with deep practical experience in product management, data analysis, and team collaboration.
【Behavior Pattern】
Zhang San's work pattern shows distinct periodicity: Mondays are typically used for planning and meetings, Wednesdays and Thursdays focus on product design and user research, and Fridays are for summary and review. He tends to brainstorm in the morning and handle execution tasks in the afternoon. April and October are his most active periods each year.
【Key Findings】
• In product decisions, Zhang San always prioritizes user feedback, as evidenced in 68% of decision records
• He excels at using data visualization tools to support arguments, a habit that plays an important role in project management
• Among team member evaluations, "clear thinking" and "quick thinking" are the most frequently mentioned keywords
• He maintains continuous attention to AI and machine learning, attending 7 related training sessions in the past 3 months
【Growth Trajectory】
Growing from a product manager at entry to a senior product manager, Zhang San has shown significant improvement in product planning, team management, and technical understanding. Especially in the past year, he has begun to independently lead more complex projects, demonstrating stronger strategic thinking capabilities. His 47 shared memories with Li Ming bear witness to his growth journey.
{% endif %}
===End of Example===
@@ -133,20 +210,40 @@ After generating the report, perform the following self-review steps:
===Output Requirements===
{% if language == "zh" %}
**语言要求:**
- 输出语言必须始终为简体中文
- 所有章节内容必须使用中文
- 章节标题必须使用指定的中文格式:【总体概述】【行为模式】【关键发现】【成长轨迹】
**格式要求:**
- 每个章节必须以标题开头,标题独占一行
- 内容紧跟标题之后
- 章节之间用空行分隔
- 关键发现章节必须使用项目符号(•)
- 严格遵守每个章节的字数限制
**内容要求:**
- 仅使用提供的数据点
- 不得捏造或推测信息
- 如果某个章节数据不足,请简要说明或跳过
- 全文保持专业、分析性的语气
{% else %}
**LANGUAGE REQUIREMENT:**
- The output language should ALWAYS be Chinese (Simplified)
- All section content must be in Chinese
- Section headers must use the specified Chinese format: 【总体概述】【行为模式】【关键发现】【成长轨迹
- The output language must ALWAYS be English
- All section content must be in English
- Section headers must use the specified English format: 【Overview】【Behavior Pattern】【Key Findings】【Growth Trajectory
**FORMAT REQUIREMENT:**
- Each section must start with its header on a new line
- Content follows immediately after the header
- Sections are separated by blank lines
- Key Findings section must use bullet points (•)
- Strictly adhere to character limits for each section
- Strictly adhere to word limits for each section
**CONTENT REQUIREMENT:**
- Only use provided data points
- Do not fabricate or speculate information
- If data is insufficient for a section, provide a brief note or skip
- Maintain professional, analytical tone throughout
{% endif %}

View File

@@ -7,6 +7,11 @@
Your task is to generate a comprehensive user profile based on the provided entities and statements. The profile should include four distinct sections that capture different aspects of the user's identity and characteristics.
{% if language == "zh" %}
**重要:请使用中文生成用户画像内容。**
{% else %}
**Important: Please generate the user profile content in English.**
{% endif %}
===Inputs===
{% if user_id %}
@@ -30,40 +35,73 @@ Your task is to generate a comprehensive user profile based on the provided enti
**Section-Specific Requirements:**
1. **Basic Introduction** (4-5 sentences, max 150 Chinese characters)
{% if language == "zh" %}
1. **基本介绍** (4-5句话最多150字)
- 重点:身份、职业、地点及其他基本人口统计信息
- 提供关于用户是谁的事实背景
2. **性格特点** (2-3句话最多80字)
- 重点:性格特征、行为习惯、沟通风格
- 描述用户互动和行为中可观察到的模式
3. **核心价值观** (1-2句话最多50字)
- 重点:价值观、信念、目标和愿望
- 捕捉对用户最重要的内容以及驱动其决策的因素
4. **一句话总结** (1句话最多40字)
- 提供对用户核心特质的高度浓缩描述
- 类似于捕捉其本质的个人标语或座右铭
{% else %}
1. **Basic Introduction** (4-5 sentences, max 150 words)
- Focus on: identity, occupation, location, and other basic demographic information
- Provide factual background about who the user is
2. **Personality Traits** (2-3 sentences, max 80 Chinese characters)
2. **Personality Traits** (2-3 sentences, max 80 words)
- Focus on: personality characteristics, behavioral habits, communication style
- Describe observable patterns in how the user interacts and behaves
3. **Core Values** (1-2 sentences, max 50 Chinese characters)
3. **Core Values** (1-2 sentences, max 50 words)
- Focus on: values, beliefs, goals, and aspirations
- Capture what matters most to the user and what drives their decisions
4. **One-Sentence Summary** (1 sentence, max 40 Chinese characters)
4. **One-Sentence Summary** (1 sentence, max 40 words)
- Provide a highly condensed characterization of the user's core traits
- Similar to a personal tagline or motto that captures their essence
{% endif %}
===Output Format (MUST STRICTLY FOLLOW)===
{% if language == "zh" %}
【基本介绍】
[4-5 sentences describing the user's basic identity, occupation, and location]
[4-5句话描述用户的基本身份、职业和地点]
【性格特点】
[2-3 sentences describing the user's personality traits, behavioral habits, and communication style]
[2-3句话描述用户的性格特征、行为习惯和沟通风格]
【核心价值观】
[1-2 sentences describing the user's values, beliefs, and goals]
[1-2句话描述用户的价值观、信念和目标]
【一句话总结】
[1句话提供对用户核心特质的高度浓缩总结]
{% else %}
【Basic Introduction】
[4-5 sentences describing the user's basic identity, occupation, and location]
【Personality Traits】
[2-3 sentences describing the user's personality traits, behavioral habits, and communication style]
【Core Values】
[1-2 sentences describing the user's values, beliefs, and goals]
【One-Sentence Summary】
[1 sentence providing a highly condensed summary of the user's core characteristics]
{% endif %}
===Example===
{% if language == "zh" %}
Example Input:
- User ID: user_12345
- Core Entities & Frequency: 产品经理 (15), AI (12), 深圳 (10), 数据分析 (8), 团队协作 (7)
@@ -81,6 +119,25 @@ Example Output:
【一句话总结】
"让每一个产品决策都充满温度。"
{% else %}
Example Input:
- User ID: user_12345
- Core Entities & Frequency: Product Manager (15), AI (12), San Francisco (10), Data Analysis (8), Team Collaboration (7)
- Representative Statement Samples: I have been working as a product manager in San Francisco for 5 years | I believe good products come from deep understanding of user needs | I enjoy playing a coordinating role in teams | Data-driven decision making is my work principle
Example Output:
【Basic Introduction】
This is a passionate senior product manager based in San Francisco. Over the past 5 years, they have focused on AI and data-driven product design, dedicated to creating products that truly improve users' lives. They believe good products stem from deep understanding of user needs and continuous exploration of technological possibilities.
【Personality Traits】
Outgoing personality with excellent communication skills and attention to detail. Enjoys playing a coordinating role in teams, helping everyone reach consensus. Maintains optimism when facing challenges, believing every problem has a solution.
【Core Values】
User-first, data-driven, continuous learning, team collaboration
【One-Sentence Summary】
"Making every product decision with warmth and purpose."
{% endif %}
===End of Example===
@@ -91,7 +148,7 @@ Before generating your final output, internally verify:
1. All content is grounded in provided data (no fabrication)
2. Format follows the specified structure with correct headers
3. Tone is objective, third-person, and neutral
4. All four sections are complete and within character limits
4. All four sections are complete and within character/word limits
**IMPORTANT: These checks are for your internal use only. DO NOT include them in your output.**
@@ -101,14 +158,24 @@ Before generating your final output, internally verify:
**CRITICAL: Your response must ONLY contain the four sections below. Do not include any reflection, self-review, or meta-commentary.**
**LANGUAGE REQUIREMENT:**
- The output language should ALWAYS be Chinese (Simplified)
- All section content must be in Chinese
- Section headers must use the specified Chinese format: 【基本介绍】【性格特点】【核心价值观】【一句话总结】
{% if language == "zh" %}
- 输出语言必须为简体中文
- 所有部分内容必须使用中文
- 部分标题必须使用指定的中文格式:【基本介绍】【性格特点】【核心价值观】【一句话总结】
{% else %}
- The output language must be English
- All section content must be in English
- Section headers must use the specified format: 【Basic Introduction】【Personality Traits】【Core Values】【One-Sentence Summary】
{% endif %}
**FORMAT REQUIREMENT:**
- Each section must start with its header on a new line
- Content follows immediately after the header
- Sections are separated by blank lines
- Strictly adhere to character limits for each section
- **DO NOT include any text after the 【一句话总结】 section**
{% if language == "zh" %}
- 严格遵守每个部分的字数限制
{% else %}
- Strictly adhere to word limits for each section
{% endif %}
- **DO NOT include any text after the final section**
- **DO NOT output reflection steps, self-review, or verification notes**

View File

@@ -81,6 +81,8 @@ class RedBearModelFactory:
# api_key 格式: "access_key_id:secret_access_key" 或只是 access_key_id
# region 从 base_url 或 extra_params 获取
from botocore.config import Config as BotoConfig
from app.core.models.bedrock_model_mapper import normalize_bedrock_model_id
max_pool_connections = int(os.getenv("BEDROCK_MAX_POOL_CONNECTIONS", "50"))
max_retries = int(os.getenv("BEDROCK_MAX_RETRIES", "2"))
# Configure with increased connection pool
@@ -89,8 +91,11 @@ class RedBearModelFactory:
retries={'max_attempts': max_retries, 'mode': 'adaptive'}
)
# 标准化模型 ID自动转换简化名称为完整 Bedrock Model ID
model_id = normalize_bedrock_model_id(config.model_name)
params = {
"model_id": config.model_name,
"model_id": model_id,
"config": boto_config,
**config.extra_params
}

View File

@@ -0,0 +1,188 @@
"""
AWS Bedrock 模型名称映射器
将简化的模型名称自动转换为正确的 Bedrock Model ID
"""
from typing import Optional
from app.core.logging_config import get_business_logger
logger = get_business_logger()
# Bedrock 模型名称映射表
BEDROCK_MODEL_MAPPING = {
# Claude 3.5 系列
"claude-3.5-sonnet": "anthropic.claude-3-5-sonnet-20240620-v1:0",
"claude-3-5-sonnet": "anthropic.claude-3-5-sonnet-20240620-v1:0",
"claude-sonnet-3.5": "anthropic.claude-3-5-sonnet-20240620-v1:0",
"claude-sonnet-3-5": "anthropic.claude-3-5-sonnet-20240620-v1:0",
# Claude 3 系列
"claude-3-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0",
"claude-3-haiku": "anthropic.claude-3-haiku-20240307-v1:0",
"claude-3-opus": "anthropic.claude-3-opus-20240229-v1:0",
"claude-sonnet": "anthropic.claude-3-sonnet-20240229-v1:0",
"claude-haiku": "anthropic.claude-3-haiku-20240307-v1:0",
"claude-opus": "anthropic.claude-3-opus-20240229-v1:0",
# Claude 2 系列
"claude-2": "anthropic.claude-v2",
"claude-2.1": "anthropic.claude-v2:1",
"claude-instant": "anthropic.claude-instant-v1",
# Amazon Titan 系列
"titan-text-express": "amazon.titan-text-express-v1",
"titan-text-lite": "amazon.titan-text-lite-v1",
"titan-embed-text": "amazon.titan-embed-text-v1",
"titan-embed-image": "amazon.titan-embed-image-v1",
# Meta Llama 系列
"llama3-70b": "meta.llama3-70b-instruct-v1:0",
"llama3-8b": "meta.llama3-8b-instruct-v1:0",
"llama2-70b": "meta.llama2-70b-chat-v1",
"llama2-13b": "meta.llama2-13b-chat-v1",
# Mistral 系列
"mistral-7b": "mistral.mistral-7b-instruct-v0:2",
"mixtral-8x7b": "mistral.mixtral-8x7b-instruct-v0:1",
"mistral-large": "mistral.mistral-large-2402-v1:0",
# 常见错误格式的映射
"claude-sonnet-4-5": "anthropic.claude-3-5-sonnet-20240620-v1:0", # 常见错误
"claude-4-5-sonnet": "anthropic.claude-3-5-sonnet-20240620-v1:0", # 常见错误
"claude-sonnet-4.5": "anthropic.claude-3-5-sonnet-20240620-v1:0", # 常见错误
}
def normalize_bedrock_model_id(model_name: str, region: Optional[str] = None) -> str:
"""
标准化 Bedrock 模型 ID
将简化的模型名称转换为正确的 Bedrock Model ID 格式
Args:
model_name: 模型名称(可能是简化格式或完整格式)
region: AWS 区域(可选,如 "us", "eu", "apac"
Returns:
str: 标准化的 Bedrock Model ID
Examples:
>>> normalize_bedrock_model_id("claude-sonnet-4-5")
'anthropic.claude-3-5-sonnet-20240620-v1:0'
>>> normalize_bedrock_model_id("claude-3.5-sonnet", region="eu")
'eu.anthropic.claude-3-5-sonnet-20240620-v1:0'
>>> normalize_bedrock_model_id("anthropic.claude-3-5-sonnet-20240620-v1:0")
'anthropic.claude-3-5-sonnet-20240620-v1:0'
"""
# 如果已经是正确的格式(包含 provider直接返回
if "." in model_name and not model_name.startswith(("us.", "eu.", "apac.", "sa.", "amer.", "global.", "us-gov.")):
# 检查是否是有效的 provider
provider = model_name.split(".", 1)[0]
valid_providers = ["anthropic", "amazon", "meta", "mistral", "deepseek", "openai", "ai21", "cohere", "stability"]
if provider in valid_providers:
logger.debug(f"Model ID 已经是正确格式: {model_name}")
return model_name
# 移除区域前缀(如果存在)
original_model_name = model_name
region_prefix = None
if model_name.startswith(("us.", "eu.", "apac.", "sa.", "amer.", "global.", "us-gov.")):
parts = model_name.split(".", 1)
region_prefix = parts[0]
model_name = parts[1] if len(parts) > 1 else model_name
# 转换为小写进行匹配
model_name_lower = model_name.lower()
# 尝试从映射表中查找
if model_name_lower in BEDROCK_MODEL_MAPPING:
mapped_id = BEDROCK_MODEL_MAPPING[model_name_lower]
logger.info(f"映射模型名称: {original_model_name} -> {mapped_id}")
# 如果指定了区域或原始名称包含区域前缀,添加区域前缀
if region:
mapped_id = f"{region}.{mapped_id}"
elif region_prefix:
mapped_id = f"{region_prefix}.{mapped_id}"
return mapped_id
# 如果没有找到映射,返回原始名称并记录警告
logger.warning(
f"未找到模型名称映射: {original_model_name}"
f"请确保使用正确的 Bedrock Model ID 格式,如 'anthropic.claude-3-5-sonnet-20240620-v1:0'"
)
return original_model_name
def is_bedrock_model_id(model_name: str) -> bool:
"""
检查是否是 Bedrock Model ID 格式
Args:
model_name: 模型名称
Returns:
bool: 是否是 Bedrock Model ID 格式
"""
# 移除区域前缀
if model_name.startswith(("us.", "eu.", "apac.", "sa.", "amer.", "global.", "us-gov.")):
model_name = model_name.split(".", 1)[1]
# 检查是否包含 provider
if "." not in model_name:
return False
provider = model_name.split(".", 1)[0]
valid_providers = ["anthropic", "amazon", "meta", "mistral", "deepseek", "openai", "ai21", "cohere", "stability"]
return provider in valid_providers
def get_provider_from_model_id(model_id: str) -> str:
"""
从 Bedrock Model ID 中提取 provider
Args:
model_id: Bedrock Model ID
Returns:
str: Provider 名称
Examples:
>>> get_provider_from_model_id("anthropic.claude-3-5-sonnet-20240620-v1:0")
'anthropic'
>>> get_provider_from_model_id("eu.anthropic.claude-3-5-sonnet-20240620-v1:0")
'anthropic'
"""
# 移除区域前缀
if model_id.startswith(("us.", "eu.", "apac.", "sa.", "amer.", "global.", "us-gov.")):
parts = model_id.split(".", 2)
return parts[1] if len(parts) > 1 else model_id.split(".", 1)[0]
return model_id.split(".", 1)[0]
# 添加更多映射的辅助函数
def add_model_mapping(short_name: str, full_model_id: str) -> None:
"""
添加自定义模型名称映射
Args:
short_name: 简化的模型名称
full_model_id: 完整的 Bedrock Model ID
"""
BEDROCK_MODEL_MAPPING[short_name.lower()] = full_model_id
logger.info(f"添加模型映射: {short_name} -> {full_model_id}")
def get_all_mappings() -> dict:
"""
获取所有模型名称映射
Returns:
dict: 模型名称映射字典
"""
return BEDROCK_MODEL_MAPPING.copy()

View File

@@ -1,5 +1,5 @@
provider: bedrock
enabled: true
enabled: false
models:
- name: ai21
type: llm

View File

@@ -1,5 +1,5 @@
provider: dashscope
enabled: true
enabled: false
models:
- name: deepseek-r1-distill-qwen-14b
type: llm

View File

@@ -1,5 +1,5 @@
provider: openai
enabled: true
enabled: false
models:
- name: chatgpt-4o-latest
type: llm

View File

@@ -4,16 +4,19 @@
从文件系统加载预定义的工作流模板
"""
import os
from pathlib import Path
from typing import Optional
import yaml
TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
class TemplateLoader:
"""工作流模板加载器"""
def __init__(self, templates_dir: str = "app/templates/workflows"):
def __init__(self, templates_dir: str = TEMPLATE_DIR):
"""初始化模板加载器
Args:
@@ -22,7 +25,7 @@ class TemplateLoader:
self.templates_dir = Path(templates_dir)
if not self.templates_dir.exists():
raise ValueError(f"模板目录不存在: {templates_dir}")
def list_templates(self) -> list[dict]:
"""列出所有可用的模板
@@ -30,22 +33,22 @@ class TemplateLoader:
模板列表,每个模板包含 id, name, description 等信息
"""
templates = []
# 遍历模板目录
for template_dir in self.templates_dir.iterdir():
if not template_dir.is_dir():
continue
# 检查是否有 template.yml 文件
template_file = template_dir / "template.yml"
if not template_file.exists():
continue
try:
# 读取模板配置
with open(template_file, 'r', encoding='utf-8') as f:
template_data = yaml.safe_load(f)
# 提取模板信息
templates.append({
"id": template_dir.name,
@@ -59,9 +62,9 @@ class TemplateLoader:
except Exception as e:
print(f"加载模板 {template_dir.name} 失败: {e}")
continue
return templates
def load_template(self, template_id: str) -> Optional[dict]:
"""加载指定的模板
@@ -73,14 +76,14 @@ class TemplateLoader:
"""
template_dir = self.templates_dir / template_id
template_file = template_dir / "template.yml"
if not template_file.exists():
return None
try:
with open(template_file, 'r', encoding='utf-8') as f:
template_data = yaml.safe_load(f)
# 返回工作流配置部分
return {
"name": template_data.get("name", template_id),
@@ -94,7 +97,7 @@ class TemplateLoader:
except Exception as e:
print(f"加载模板 {template_id} 失败: {e}")
return None
def get_template_readme(self, template_id: str) -> Optional[str]:
"""获取模板的 README 文档
@@ -106,10 +109,10 @@ class TemplateLoader:
"""
template_dir = self.templates_dir / template_id
readme_file = template_dir / "README.md"
if not readme_file.exists():
return None
try:
with open(readme_file, 'r', encoding='utf-8') as f:
return f.read()

View File

@@ -26,6 +26,9 @@ from .app_schema import (
MemoryConfig,
ToolConfig,
VariableDefinition,
FileInput,
FileType,
TransferMethod,
)
from .conversation_schema import (
Conversation,
@@ -94,6 +97,9 @@ __all__ = [
"MemoryConfig",
"ToolConfig",
"VariableDefinition",
"FileInput",
"FileType",
"TransferMethod",
"Conversation",
"ConversationCreate",
"ConversationWithMessages",

View File

@@ -1,10 +1,51 @@
import datetime
import uuid
from typing import Optional, Any, List, Dict, Union
from enum import Enum
from pydantic import BaseModel, Field, ConfigDict, field_serializer, field_validator
# ---------- Multimodal File Support ----------
class FileType(str, Enum):
"""文件类型枚举"""
IMAGE = "image"
DOCUMENT = "document"
AUDIO = "audio"
VIDEO = "video"
class TransferMethod(str, Enum):
"""文件传输方式枚举"""
LOCAL_FILE = "local_file" # 已上传到系统的文件
REMOTE_URL = "remote_url" # 外部URL
class FileInput(BaseModel):
"""文件输入 Schema"""
type: FileType = Field(..., description="文件类型: image/document/audio/video")
transfer_method: TransferMethod = Field(..., description="传输方式: local_file/remote_url")
upload_file_id: Optional[uuid.UUID] = Field(None, description="已上传文件IDlocal_file时必填")
url: Optional[str] = Field(None, description="远程URLremote_url时必填")
@field_validator("upload_file_id")
@classmethod
def validate_local_file(cls, v, info):
"""验证 local_file 时必须提供 upload_file_id"""
if info.data.get("transfer_method") == TransferMethod.LOCAL_FILE and not v:
raise ValueError("transfer_method 为 local_file 时upload_file_id 不能为空")
return v
@field_validator("url")
@classmethod
def validate_remote_url(cls, v, info):
"""验证 remote_url 时必须提供 url"""
if info.data.get("transfer_method") == TransferMethod.REMOTE_URL and not v:
raise ValueError("transfer_method 为 remote_url 时url 不能为空")
return v
# ---------- Input Schemas ----------
class KnowledgeBaseConfig(BaseModel):
@@ -360,6 +401,7 @@ class AppChatRequest(BaseModel):
user_id: Optional[str] = Field(default=None, description="用户ID用于会话管理")
variables: Optional[Dict[str, Any]] = Field(default=None, description="自定义变量参数值")
stream: bool = Field(default=False, description="是否流式返回")
files: Optional[List[FileInput]] = Field(default=None, description="附件列表(支持多文件)")
class DraftRunRequest(BaseModel):
@@ -369,6 +411,7 @@ class DraftRunRequest(BaseModel):
user_id: Optional[str] = Field(default=None, description="用户ID用于会话管理")
variables: Optional[Dict[str, Any]] = Field(default=None, description="自定义变量参数值")
stream: bool = Field(default=False, description="是否流式返回")
files: Optional[List[FileInput]] = Field(default=None, description="附件列表(支持多文件)")
class DraftRunResponse(BaseModel):

View File

@@ -4,6 +4,9 @@ import datetime
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field, ConfigDict, field_serializer
# 导入 FileInput用于体验运行
from app.schemas.app_schema import FileInput
# ---------- Input Schemas ----------
@@ -28,6 +31,7 @@ class ChatRequest(BaseModel):
stream: bool = Field(default=False, description="是否流式返回")
web_search: bool = Field(default=False, description="是否启用网络搜索")
memory: bool = Field(default=True, description="是否启用记忆功能")
files: Optional[List[FileInput]] = Field(default=None, description="附件列表(支持多文件)")
# ---------- Output Schemas ----------

View File

@@ -3,7 +3,7 @@ import asyncio
import json
import time
import uuid
from typing import Optional, Dict, Any, AsyncGenerator, Annotated
from typing import Optional, Dict, Any, AsyncGenerator, Annotated, List
from fastapi import Depends
from sqlalchemy.orm import Session
@@ -15,6 +15,7 @@ from app.core.logging_config import get_business_logger
from app.db import get_db, get_db_context
from app.models import MultiAgentConfig, AgentConfig, WorkflowConfig
from app.schemas import DraftRunRequest
from app.schemas.app_schema import FileInput
from app.services.tool_service import ToolService
from app.repositories.tool_repository import ToolRepository
from app.db import get_db
@@ -26,6 +27,7 @@ from app.services.draft_run_service import create_web_search_tool
from app.services.model_service import ModelApiKeyService
from app.services.multi_agent_orchestrator import MultiAgentOrchestrator
from app.services.workflow_service import WorkflowService
from app.services.multimodal_service import MultimodalService
logger = get_business_logger()
@@ -48,7 +50,8 @@ class AppChatService:
memory: bool = True,
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
workspace_id: Optional[str] = None
workspace_id: Optional[str] = None,
files: Optional[List[FileInput]] = None # 新增:多模态文件
) -> Dict[str, Any]:
"""聊天(非流式)"""
@@ -155,7 +158,14 @@ class AppChatService:
for msg in messages
]
# 调用 Agent
# 处理多模态文件
processed_files = None
if files:
multimodal_service = MultimodalService(self.db)
processed_files = await multimodal_service.process_files(files)
logger.info(f"处理了 {len(processed_files)} 个文件")
# 调用 Agent支持多模态
result = await agent.chat(
message=message,
history=history,
@@ -164,7 +174,8 @@ class AppChatService:
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
config_id=config_id,
memory_flag=memory_flag
memory_flag=memory_flag,
files=processed_files # 传递处理后的文件
)
# 保存消息
@@ -206,6 +217,7 @@ class AppChatService:
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
workspace_id: Optional[str] = None,
files: Optional[List[FileInput]] = None # 新增:多模态文件
) -> AsyncGenerator[str, None]:
"""聊天(流式)"""
@@ -312,10 +324,17 @@ class AppChatService:
for msg in messages
]
# 处理多模态文件
processed_files = None
if files:
multimodal_service = MultimodalService(self.db)
processed_files = await multimodal_service.process_files(files)
logger.info(f"处理了 {len(processed_files)} 个文件")
# 发送开始事件
yield f"event: start\ndata: {json.dumps({'conversation_id': str(conversation_id)}, ensure_ascii=False)}\n\n"
# 流式调用 Agent
# 流式调用 Agent(支持多模态)
full_content = ""
total_tokens = 0
async for chunk in agent.chat_stream(
@@ -326,7 +345,8 @@ class AppChatService:
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
config_id=config_id,
memory_flag=memory_flag
memory_flag=memory_flag,
files=processed_files # 传递处理后的文件
):
if isinstance(chunk, int):
total_tokens = chunk

View File

@@ -19,11 +19,13 @@ from app.models import AgentConfig, ModelApiKey, ModelConfig
from app.repositories.model_repository import ModelApiKeyRepository
from app.repositories.tool_repository import ToolRepository
from app.schemas.prompt_schema import PromptMessageRole, render_prompt_message
from app.schemas.app_schema import FileInput
from app.services import task_service
from app.services.langchain_tool_server import Search
from app.services.memory_agent_service import MemoryAgentService
from app.services.model_parameter_merger import ModelParameterMerger
from app.services.tool_service import ToolService
from app.services.multimodal_service import MultimodalService
from langchain.tools import tool
from pydantic import BaseModel, Field
from sqlalchemy import select
@@ -62,26 +64,23 @@ def create_long_term_memory_tool(memory_config: Dict[str, Any], end_user_id: str
@tool(args_schema=LongTermMemoryInput)
def long_term_memory(question: str) -> str:
"""
从用户的历史记忆中检索相关信息。这是一个强大的工具,可以帮助你了解用户的背景、偏好和历史对话内容。
从用户的历史记忆中检索相关信息。用于了解用户的背景、偏好和历史对话内容。
以下场景不需要使用此工具:
1. 情绪/社交问候场景(如"你好""谢谢""再见"等简单寒暄
2. 纯任务性场景(如"帮我写代码""翻译这段文字"等不需要历史上下文的任务
3. 处理外部内容时如用户提供的文本、代码、RAG数据等这些内容本身已经包含所需信息
**何时使用此工具:**
- 用户明确询问历史信息(如"我之前说过什么""上次我们聊了什么"
- 用户询问个人信息或偏好(如"我喜欢什么""我的习惯是什么"
- 需要基于历史上下文提供个性化建议
除上述场景外的所有其他情况都应该使用此工具,特别是:
- 用户询问个人信息或历史对话内容
- 需要了解用户偏好、习惯或背景
- 用户提到"之前""上次""记得"等涉及历史的词汇
- 需要个性化回复或基于历史上下文的建议
- 用户询问关于自己的任何信息
**何时不使用此工具:**
- 简单问候(如"你好""谢谢""再见"
- 纯任务性请求(如"写代码""翻译文字""分析图片"
- 用户已提供完整信息(如提供了文本、图片、文档等内容)
- 创作性任务(如"写诗""编故事""创作谜语"
**重要:如果用户的问题可以直接回答,不要调用此工具。只在确实需要历史信息时才使用。**
需要对question改写/优化:
需要重点关注一以下几点
- 相关的关键词,保持原问题的核心语义不变, 根据上下文,使问题更具体、更清晰,将模糊的表达转换为明确的搜索词
- 使用同义词或相关术语扩展查询
Args:
question: question改写之后的内容
question: 需要检索的问题(保持原问题的核心语义,使用清晰的关键词)
Returns:
检索到的历史记忆内容
@@ -124,6 +123,10 @@ def create_long_term_memory_tool(memory_config: Dict[str, Any], end_user_id: str
}
)
# 检查是否有有效内容
if not memory_content or str(memory_content).strip() == "" or "answer" in str(memory_content) and str(memory_content).count("''") > 0:
return "未找到相关的历史记忆。请直接回答用户的问题,不要再次调用此工具。"
return f"检索到以下历史记忆:\n\n{memory_content}"
except Exception as e:
logger.error("长期记忆检索失败", extra={"error": str(e), "error_type": type(e).__name__})
@@ -246,7 +249,8 @@ class DraftRunService:
user_rag_memory_id: Optional[str] = None,
web_search: bool = True,
memory: bool = True,
sub_agent: bool = False
sub_agent: bool = False,
files: Optional[List[FileInput]] = None # 新增:多模态文件
) -> Dict[str, Any]:
"""执行试运行(使用 LangChain Agent
@@ -406,7 +410,16 @@ class DraftRunService:
max_history=agent_config.memory.get("max_history", 10)
)
# 6. 知识库检索
# 6. 处理多模态文件
processed_files = None
if files:
# 获取 provider 信息
provider = api_key_config.get("provider", "openai")
multimodal_service = MultimodalService(self.db, provider=provider)
processed_files = await multimodal_service.process_files(files)
logger.info(f"处理了 {len(processed_files)} 个文件provider={provider}")
# 7. 知识库检索
context = None
logger.debug(
@@ -414,14 +427,15 @@ class DraftRunService:
extra={
"model": api_key_config["model_name"],
"has_history": bool(history),
"has_context": bool(context)
"has_context": bool(context),
"has_files": bool(processed_files)
}
)
memory_config_= agent_config.memory
config_id = memory_config_.get("memory_content") or memory_config_.get("memory_config",None)
# 7. 调用 Agent
# 8. 调用 Agent(支持多模态)
result = await agent.chat(
message=message,
history=history,
@@ -430,12 +444,13 @@ class DraftRunService:
config_id=config_id,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
memory_flag=memory_flag
memory_flag=memory_flag,
files=processed_files # 传递处理后的文件
)
elapsed_time = time.time() - start_time
# 8. 保存会话消息
# 9. 保存会话消息
if not sub_agent and agent_config.memory and agent_config.memory.get("enabled"):
await self._save_conversation_message(
conversation_id=conversation_id,
@@ -493,7 +508,8 @@ class DraftRunService:
user_rag_memory_id: Optional[str] = None,
web_search: bool = True, # 布尔类型默认值
memory: bool = True, # 布尔类型默认值
sub_agent: bool = False # 是否是作为子Agent运行
sub_agent: bool = False, # 是否是作为子Agent运行
files: Optional[List[FileInput]] = None # 新增:多模态文件
) -> AsyncGenerator[str, None]:
"""执行试运行(流式返回,使用 LangChain Agent
@@ -642,6 +658,15 @@ class DraftRunService:
max_history=agent_config.memory.get("max_history", 10)
)
# 6. 处理多模态文件
processed_files = None
if files:
# 获取 provider 信息
provider = api_key_config.get("provider", "openai")
multimodal_service = MultimodalService(self.db, provider=provider)
processed_files = await multimodal_service.process_files(files)
logger.info(f"处理了 {len(processed_files)} 个文件provider={provider}")
# 7. 知识库检索
context = None
@@ -654,7 +679,7 @@ class DraftRunService:
memory_config_ = agent_config.memory
config_id = memory_config_.get("memory_content") or memory_config_.get("memory_config",None)
# 9. 流式调用 Agent
# 9. 流式调用 Agent(支持多模态)
full_content = ""
total_tokens = 0
async for chunk in agent.chat_stream(
@@ -665,7 +690,8 @@ class DraftRunService:
config_id=config_id,
storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id,
memory_flag=memory_flag
memory_flag=memory_flag,
files=processed_files # 传递处理后的文件
):
if isinstance(chunk, int):
total_tokens = chunk

View File

@@ -57,24 +57,57 @@ class EmotionAnalyticsService:
self.emotion_repo = EmotionRepository(connector)
logger.info("情绪分析服务初始化完成")
# 情绪类型的中英文映射
EMOTION_TYPE_TRANSLATIONS = {
'joy': {'zh': '喜悦', 'en': 'Joy'},
'sadness': {'zh': '悲伤', 'en': 'Sadness'},
'anger': {'zh': '愤怒', 'en': 'Anger'},
'fear': {'zh': '恐惧', 'en': 'Fear'},
'surprise': {'zh': '惊讶', 'en': 'Surprise'},
'neutral': {'zh': '中性', 'en': 'Neutral'}
}
def _translate_emotion_type(self, emotion_type: str, language: str = "zh") -> str:
"""将情绪类型翻译成指定语言
Args:
emotion_type: 情绪类型英文key
language: 目标语言 ("zh""en")
Returns:
翻译后的情绪类型名称
"""
if emotion_type in self.EMOTION_TYPE_TRANSLATIONS:
return self.EMOTION_TYPE_TRANSLATIONS[emotion_type].get(language, emotion_type)
return emotion_type
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
limit: int = 10,
language: str = "zh"
) -> Dict[str, Any]:
"""获取情绪标签统计
查询指定用户的情绪类型分布,包括计数、百分比和平均强度。
确保返回所有6个情绪维度joy、sadness、anger、fear、surprise、neutral
即使某些维度没有数据也会返回count=0的记录。
Args:
end_user_id: 用户ID
emotion_type: 情绪类型过滤
start_date: 开始日期
end_date: 结束日期
limit: 返回数量限制
language: 输出语言 ("zh" 中文, "en" 英文)
"""
try:
logger.info(f"获取情绪标签统计: user={end_user_id}, type={emotion_type}, "
f"start={start_date}, end={end_date}, limit={limit}")
f"start={start_date}, end={end_date}, limit={limit}, language={language}")
# 调用仓储层查询
tags = await self.emotion_repo.get_emotion_tags(
@@ -91,15 +124,17 @@ class EmotionAnalyticsService:
# 将查询结果转换为字典,方便查找
tags_dict = {tag["emotion_type"]: tag for tag in tags}
# 补全缺失的情绪维度
# 补全缺失的情绪维度,并翻译 emotion_type
complete_tags = []
for emotion in all_emotion_types:
if emotion in tags_dict:
complete_tags.append(tags_dict[emotion])
tag = tags_dict[emotion].copy()
tag["emotion_type"] = self._translate_emotion_type(emotion, language)
complete_tags.append(tag)
else:
# 如果该情绪类型不存在,添加默认值
complete_tags.append({
"emotion_type": emotion,
"emotion_type": self._translate_emotion_type(emotion, language),
"count": 0,
"percentage": 0.0,
"avg_intensity": 0.0
@@ -475,6 +510,7 @@ class EmotionAnalyticsService:
self,
end_user_id: str,
db: Session,
language: str = "zh",
) -> Dict[str, Any]:
"""生成个性化情绪建议
@@ -483,6 +519,7 @@ class EmotionAnalyticsService:
Args:
end_user_id: 宿主ID用户组ID
db: 数据库会话
language: 输出语言 ("zh" 中文, "en" 英文)
Returns:
Dict: 包含个性化建议的响应:
@@ -533,7 +570,7 @@ class EmotionAnalyticsService:
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)
prompt = await self._build_suggestion_prompt(health_data, patterns, user_profile, language)
# 7. 调用LLM生成建议使用配置中的LLM
if llm_client is None:
@@ -554,12 +591,12 @@ class EmotionAnalyticsService:
except Exception as e:
logger.error(f"LLM 结构化输出失败: {str(e)}")
# 返回默认建议
suggestions_response = self._get_default_suggestions(health_data)
suggestions_response = self._get_default_suggestions(health_data, language)
# 8. 验证建议数量3-5条
if len(suggestions_response.suggestions) < 3:
logger.warning(f"建议数量不足: {len(suggestions_response.suggestions)}")
suggestions_response = self._get_default_suggestions(health_data)
suggestions_response = self._get_default_suggestions(health_data, language)
elif len(suggestions_response.suggestions) > 5:
logger.warning(f"建议数量过多: {len(suggestions_response.suggestions)}")
suggestions_response.suggestions = suggestions_response.suggestions[:5]
@@ -624,7 +661,8 @@ class EmotionAnalyticsService:
self,
health_data: Dict[str, Any],
patterns: Dict[str, Any],
user_profile: Dict[str, Any]
user_profile: Dict[str, Any],
language: str = "zh"
) -> str:
"""构建情绪建议生成的prompt
@@ -632,6 +670,7 @@ class EmotionAnalyticsService:
health_data: 情绪健康数据
patterns: 情绪模式分析结果
user_profile: 用户画像数据
language: 输出语言 ("zh" 中文, "en" 英文)
Returns:
str: LLM prompt
@@ -643,66 +682,114 @@ class EmotionAnalyticsService:
prompt = await render_emotion_suggestions_prompt(
health_data=health_data,
patterns=patterns,
user_profile=user_profile
user_profile=user_profile,
language=language
)
return prompt
def _get_default_suggestions(self, health_data: Dict[str, Any]) -> EmotionSuggestionsResponse:
def _get_default_suggestions(self, health_data: Dict[str, Any], language: str = "zh") -> EmotionSuggestionsResponse:
"""获取默认建议当LLM调用失败时使用
Args:
health_data: 情绪健康数据
language: 输出语言 ("zh" 中文, "en" 英文)
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 = "您的情绪健康需要重点关注,建议寻求专业帮助。"
if language == "en":
if health_score >= 80:
summary = "Your emotional health is excellent. Keep up the positive attitude."
elif health_score >= 60:
summary = "Your emotional health is good. Some adjustments can further improve it."
elif health_score >= 40:
summary = "Your emotional health needs attention. Consider taking improvement measures."
else:
summary = "Your emotional health needs serious attention. Consider seeking professional help."
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-330分钟的散步",
"周末尝试户外运动如骑行或爬山",
"在户外活动时关注周围环境,放松心情"
]
),
EmotionSuggestion(
type="social_connection",
title="加强社交联系",
content="与朋友和家人保持良好的社交联系,可以提供情感支持,改善情绪健康。",
priority="medium",
actionable_steps=[
"每周至少与一位朋友或家人深入交流",
"参加感兴趣的社交活动或兴趣小组",
"主动分享自己的感受和想法"
]
)
]
suggestions = [
EmotionSuggestion(
type="Emotion Balance",
title="Maintain Emotional Balance",
content="Through mindfulness meditation and deep breathing exercises, help you better manage emotional fluctuations and improve emotional stability.",
priority="High",
actionable_steps=[
"Practice 5-10 minutes of mindfulness meditation every morning",
"Take 3 deep breaths when feeling emotional fluctuations",
"Record daily emotional changes to identify triggers"
]
),
EmotionSuggestion(
type="Activity Recommendation",
title="Increase Outdoor Activities",
content="Moderate outdoor exercise can effectively improve mood and enhance physical and mental health. Recommend 3-4 outdoor activities per week.",
priority="Medium",
actionable_steps=[
"Schedule 2-3 30-minute walks per week",
"Try outdoor sports like cycling or hiking on weekends",
"Focus on surroundings and relax during outdoor activities"
]
),
EmotionSuggestion(
type="Social Connection",
title="Strengthen Social Connections",
content="Maintaining good social connections with friends and family can provide emotional support and improve emotional health.",
priority="Medium",
actionable_steps=[
"Have a deep conversation with at least one friend or family member weekly",
"Join social activities or interest groups you enjoy",
"Actively share your feelings and thoughts"
]
)
]
else:
if health_score >= 80:
summary = "您的情绪健康状况优秀,请继续保持积极的生活态度。"
elif health_score >= 60:
summary = "您的情绪健康状况良好,可以通过一些调整进一步提升。"
elif health_score >= 40:
summary = "您的情绪健康需要关注,建议采取一些改善措施。"
else:
summary = "您的情绪健康需要重点关注,建议寻求专业帮助。"
suggestions = [
EmotionSuggestion(
type="情绪平衡",
title="保持情绪平衡",
content="通过正念冥想和深呼吸练习,帮助您更好地管理情绪波动,提升情绪稳定性。",
priority="",
actionable_steps=[
"每天早晨进行5-10分钟的正念冥想",
"感到情绪波动时进行3次深呼吸",
"记录每天的情绪变化,识别触发因素"
]
),
EmotionSuggestion(
type="活动建议",
title="增加户外活动",
content="适度的户外运动可以有效改善情绪增强身心健康。建议每周进行3-4次户外活动。",
priority="",
actionable_steps=[
"每周安排2-3次30分钟的散步",
"周末尝试户外运动如骑行或爬山",
"在户外活动时关注周围环境,放松心情"
]
),
EmotionSuggestion(
type="社交联系",
title="加强社交联系",
content="与朋友和家人保持良好的社交联系,可以提供情感支持,改善情绪健康。",
priority="",
actionable_steps=[
"每周至少与一位朋友或家人深入交流",
"参加感兴趣的社交活动或兴趣小组",
"主动分享自己的感受和想法"
]
)
]
return EmotionSuggestionsResponse(
health_summary=summary,

View File

@@ -37,7 +37,6 @@ from app.repositories.memory_short_repository import ShortTermMemoryRepository
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
from app.schemas.memory_agent_schema import Write_UserInput
from app.schemas.memory_config_schema import ConfigurationError
from app.services.memory_base_service import Translation_English
from app.services.memory_config_service import MemoryConfigService
from app.services.memory_konwledges_server import (
write_rag,
@@ -265,7 +264,7 @@ class MemoryAgentService:
logger.info("Log streaming completed, cleaning up resources")
# LogStreamer uses context manager for file handling, so cleanup is automatic
async def write_memory(self, end_user_id: str, messages: list[dict], config_id: Optional[uuid.UUID]|int, db: Session, storage_type: str, user_rag_memory_id: str) -> str:
async def write_memory(self, end_user_id: str, messages: list[dict], config_id: Optional[uuid.UUID]|int, db: Session, storage_type: str, user_rag_memory_id: str, language: str = "zh") -> str:
"""
Process write operation with config_id
@@ -276,6 +275,7 @@ class MemoryAgentService:
db: SQLAlchemy database session
storage_type: Storage type (neo4j or rag)
user_rag_memory_id: User RAG memory ID
language: 语言类型 ("zh" 中文, "en" 英文)
Returns:
Write operation result status
@@ -341,7 +341,8 @@ class MemoryAgentService:
initial_state = {
"messages": langchain_messages,
"end_user_id": end_user_id,
"memory_config": memory_config
"memory_config": memory_config,
"language": language
}
# 获取节点更新信息
@@ -896,9 +897,7 @@ class MemoryAgentService:
async def get_hot_memory_tags_by_user(
self,
end_user_id: Optional[str] = None,
limit: int = 20,
model_id: Optional[str] = None,
language_type: Optional[str] = "zh"
limit: int = 20
) -> List[Dict[str, Any]]:
"""
获取指定用户的热门记忆标签
@@ -912,17 +911,15 @@ class MemoryAgentService:
{"name": "标签名", "frequency": 频次},
...
]
注意:标签语言由写入时的 X-Language-Type 决定,查询时不进行翻译
"""
try:
# by_user=False 表示按 end_user_id 查询在Neo4j中end_user_id就是用户维度
tags = await get_hot_memory_tags(end_user_id, limit=limit, by_user=False)
payload=[]
payload = []
for tag, freq in tags:
if language_type!="zh":
tag=await Translation_English(model_id, tag)
payload.append({"name": tag, "frequency": freq})
else:
payload.append({"name": tag, "frequency": freq})
payload.append({"name": tag, "frequency": freq})
return payload
except Exception as e:
logger.error(f"热门记忆标签查询失败: {e}")

View File

@@ -16,7 +16,6 @@ import json
from datetime import datetime
from app.schemas.memory_episodic_schema import EmotionType
from app.services.memory_base_service import Translation_English
logger = logging.getLogger(__name__)
@@ -374,119 +373,6 @@ class MemoryEntityService:
logger.warning(f"转换时间格式失败: {e}, 原始值: {dt}")
return str(dt) if dt is not None else None
async def _translate_list(
self,
data_list: List[Dict[str, Any]],
model_id: str,
fields: List[str]
) -> List[Dict[str, Any]]:
"""
翻译列表中每个字典的指定字段(并发有限度以降低整体延迟)
Args:
data_list: 要翻译的字典列表
model_id: 模型ID
fields: 需要翻译的字段列表
Returns:
翻译后的字典列表
"""
# 空列表或无字段时直接返回
if not data_list or not fields:
return data_list
import asyncio
# 并发限制,避免一次性发起过多请求
# 可根据实际情况调整(建议 5-10
concurrency_limit = 5
semaphore = asyncio.Semaphore(concurrency_limit)
async def translate_single_field(
index: int,
field: str,
value: Any,
) -> Optional[tuple]:
"""
翻译单个字段并返回 (索引, 字段名, 翻译结果)
Returns:
(index, field, translated_value) 或 None如果跳过
"""
# 跳过空值
if value is None or value == "":
return None
# 统一转成字符串再翻译,防止非字符串类型导致错误
text = str(value)
try:
async with semaphore:
# 调用 Translation_English 进行翻译
# 注意Translation_English 的参数顺序是 (model_id, text)
translated = await Translation_English(model_id, text)
# 如果翻译结果为空,保留原值
if translated is None or translated == "":
return None
return index, field, translated
except Exception as e:
logger.warning(f"翻译字段 {field} (索引 {index}) 失败: {e}")
return None
# 构造所有需要翻译的任务
tasks = []
for idx, item in enumerate(data_list):
# 防御性检查:确保 item 是字典
if not isinstance(item, dict):
continue
for field in fields:
if field not in item:
continue
value = item.get(field)
# 对于 None 或空字符串的值,直接跳过,不创建任务
if value is None or value == "":
continue
tasks.append(
asyncio.create_task(
translate_single_field(idx, field, value)
)
)
# 如果没有需要翻译的任务,直接返回原列表
if not tasks:
return data_list
# 使用 gather 并发执行翻译任务(受 semaphore 限制)
# return_exceptions=True 可以防止单个任务失败导致整体失败
results = await asyncio.gather(*tasks, return_exceptions=True)
# 创建深拷贝以避免修改原始数据
translated_list = [item.copy() if isinstance(item, dict) else item for item in data_list]
# 将翻译结果回填到列表
for result in results:
# 跳过 None 结果和异常
if result is None or isinstance(result, Exception):
if isinstance(result, Exception):
logger.warning(f"翻译任务异常: {result}")
continue
idx, field, translated = result
# 防御性检查索引范围
if 0 <= idx < len(translated_list) and isinstance(translated_list[idx], dict):
translated_list[idx][field] = translated
return translated_list
async def close(self):
"""关闭数据库连接"""

View File

@@ -236,12 +236,13 @@ class DataConfigService: # 数据配置服务类PostgreSQL
return self._convert_timestamps_to_format(data_list)
async def pilot_run_stream(self, payload: ConfigPilotRun) -> AsyncGenerator[str, None]:
async def pilot_run_stream(self, payload: ConfigPilotRun, language: str = "zh") -> AsyncGenerator[str, None]:
"""
流式执行试运行,产生 SSE 格式的进度事件
Args:
payload: 试运行配置和对话文本
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
Yields:
SSE 格式的字符串,包含以下事件类型:
@@ -315,6 +316,7 @@ class DataConfigService: # 数据配置服务类PostgreSQL
dialogue_text=dialogue_text,
db=self.db,
progress_callback=progress_callback,
language=language,
)
logger.info("[PILOT_RUN_STREAM] pipeline_main completed")

View File

@@ -0,0 +1,429 @@
"""
多模态文件处理服务
处理图片、文档等多模态文件,转换为 LLM 可用的格式
支持的 Provider:
- DashScope (通义千问): 支持 URL 格式
- Bedrock/Anthropic: 仅支持 base64 格式
- OpenAI: 支持 URL 和 base64 格式
"""
import uuid
from typing import List, Dict, Any, Optional, Protocol
from sqlalchemy.orm import Session
from app.core.logging_config import get_business_logger
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
from app.schemas.app_schema import FileInput, FileType, TransferMethod
from app.models.generic_file_model import GenericFile
logger = get_business_logger()
class ImageFormatStrategy(Protocol):
"""图片格式策略接口"""
async def format_image(self, url: str) -> Dict[str, Any]:
"""将图片 URL 转换为特定 provider 的格式"""
...
class DashScopeImageStrategy:
"""通义千问图片格式策略"""
async def format_image(self, url: str) -> Dict[str, Any]:
"""通义千问格式: {"type": "image", "image": "url"}"""
return {
"type": "image",
"image": url
}
class BedrockImageStrategy:
"""Bedrock/Anthropic 图片格式策略"""
async def format_image(self, url: str) -> Dict[str, Any]:
"""
Bedrock/Anthropic 格式: base64 编码
{"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}}
"""
import httpx
import base64
from mimetypes import guess_type
logger.info(f"下载并编码图片: {url}")
# 下载图片
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(url)
response.raise_for_status()
# 获取图片数据
image_data = response.content
# 确定 media type
content_type = response.headers.get("content-type")
if content_type and content_type.startswith("image/"):
media_type = content_type
else:
guessed_type, _ = guess_type(url)
media_type = guessed_type if guessed_type and guessed_type.startswith("image/") else "image/jpeg"
# 转换为 base64
base64_data = base64.b64encode(image_data).decode("utf-8")
logger.info(f"图片编码完成: media_type={media_type}, size={len(base64_data)}")
return {
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data
}
}
class OpenAIImageStrategy:
"""OpenAI 图片格式策略"""
async def format_image(self, url: str) -> Dict[str, Any]:
"""OpenAI 格式: {"type": "image_url", "image_url": {"url": "..."}}"""
return {
"type": "image_url",
"image_url": {
"url": url
}
}
# Provider 到策略的映射
PROVIDER_STRATEGIES = {
"dashscope": DashScopeImageStrategy,
"bedrock": BedrockImageStrategy,
"anthropic": BedrockImageStrategy,
"openai": OpenAIImageStrategy,
}
class MultimodalService:
"""多模态文件处理服务"""
def __init__(self, db: Session, provider: str = "dashscope"):
"""
初始化多模态服务
Args:
db: 数据库会话
provider: 模型提供商dashscope, bedrock, anthropic 等)
"""
self.db = db
self.provider = provider.lower()
async def process_files(
self,
files: Optional[List[FileInput]]
) -> List[Dict[str, Any]]:
"""
处理文件列表,返回 LLM 可用的格式
Args:
files: 文件输入列表
Returns:
List[Dict]: LLM 可用的内容格式列表(根据 provider 返回不同格式)
"""
if not files:
return []
result = []
for idx, file in enumerate(files):
try:
if file.type == FileType.IMAGE:
content = await self._process_image(file)
result.append(content)
elif file.type == FileType.DOCUMENT:
content = await self._process_document(file)
result.append(content)
elif file.type == FileType.AUDIO:
content = await self._process_audio(file)
result.append(content)
elif file.type == FileType.VIDEO:
content = await self._process_video(file)
result.append(content)
else:
logger.warning(f"不支持的文件类型: {file.type}")
except Exception as e:
logger.error(
f"处理文件失败",
extra={
"file_index": idx,
"file_type": file.type,
"error": str(e)
}
)
# 继续处理其他文件,不中断整个流程
result.append({
"type": "text",
"text": f"[文件处理失败: {str(e)}]"
})
logger.info(f"成功处理 {len(result)}/{len(files)} 个文件provider={self.provider}")
return result
async def _process_image(self, file: FileInput) -> Dict[str, Any]:
"""
处理图片文件
Args:
file: 图片文件输入
Returns:
Dict: 根据 provider 返回不同格式
- Anthropic/Bedrock: {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}}
- 通义千问: {"type": "image", "image": "url"}
"""
if file.transfer_method == TransferMethod.REMOTE_URL:
url = file.url
else:
# 本地文件,获取访问 URL
url = await self._get_file_url(file.upload_file_id)
logger.debug(f"处理图片: {url}, provider={self.provider}")
# 根据 provider 返回不同格式
if self.provider in ["bedrock", "anthropic"]:
# Anthropic/Bedrock 只支持 base64 格式,需要下载并转换
try:
logger.info(f"开始下载并编码图片: {url}")
base64_data, media_type = await self._download_and_encode_image(url)
result = {
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data[:100] + "..." # 只记录前100个字符
}
}
logger.info(f"图片编码完成: media_type={media_type}, data_length={len(base64_data)}")
# 返回完整数据
result["source"]["data"] = base64_data
return result
except Exception as e:
logger.error(f"下载并编码图片失败: {e}", exc_info=True)
# 返回错误提示
return {
"type": "text",
"text": f"[图片加载失败: {str(e)}]"
}
else:
# 通义千问等其他格式支持 URL
return {
"type": "image",
"image": url
}
async def _download_and_encode_image(self, url: str) -> tuple[str, str]:
"""
下载图片并转换为 base64
Args:
url: 图片 URL
Returns:
tuple: (base64_data, media_type)
"""
import httpx
import base64
from mimetypes import guess_type
# 下载图片
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(url)
response.raise_for_status()
# 获取图片数据
image_data = response.content
# 确定 media type
content_type = response.headers.get("content-type")
if content_type and content_type.startswith("image/"):
media_type = content_type
else:
# 从 URL 推断
guessed_type, _ = guess_type(url)
media_type = guessed_type if guessed_type and guessed_type.startswith("image/") else "image/jpeg"
# 转换为 base64
base64_data = base64.b64encode(image_data).decode("utf-8")
logger.debug(f"图片编码完成: media_type={media_type}, size={len(base64_data)}")
return base64_data, media_type
async def _process_document(self, file: FileInput) -> Dict[str, Any]:
"""
处理文档文件PDF、Word 等)
Args:
file: 文档文件输入
Returns:
Dict: text 格式的内容(包含提取的文本)
"""
if file.transfer_method == TransferMethod.REMOTE_URL:
# 远程文档暂不支持提取
return {
"type": "text",
"text": f"<document url=\"{file.url}\">\n[远程文档,暂不支持内容提取]\n</document>"
}
else:
# 本地文件,提取文本内容
text = await self._extract_document_text(file.upload_file_id)
generic_file = self.db.query(GenericFile).filter(
GenericFile.id == file.upload_file_id
).first()
file_name = generic_file.file_name if generic_file else "unknown"
return {
"type": "text",
"text": f"<document name=\"{file_name}\">\n{text}\n</document>"
}
async def _process_audio(self, file: FileInput) -> Dict[str, Any]:
"""
处理音频文件
Args:
file: 音频文件输入
Returns:
Dict: 音频内容(暂时返回占位符)
"""
# TODO: 实现音频转文字功能
return {
"type": "text",
"text": "[音频文件,暂不支持处理]"
}
async def _process_video(self, file: FileInput) -> Dict[str, Any]:
"""
处理视频文件
Args:
file: 视频文件输入
Returns:
Dict: 视频内容(暂时返回占位符)
"""
# TODO: 实现视频处理功能
return {
"type": "text",
"text": "[视频文件,暂不支持处理]"
}
async def _get_file_url(self, file_id: uuid.UUID) -> str:
"""
获取文件的访问 URL
Args:
file_id: 文件ID
Returns:
str: 文件访问 URL
Raises:
BusinessException: 文件不存在
"""
generic_file = self.db.query(GenericFile).filter(
GenericFile.id == file_id,
GenericFile.status == "active"
).first()
if not generic_file:
raise BusinessException(
f"文件不存在或已删除: {file_id}",
BizCode.NOT_FOUND
)
# 如果有 access_url直接返回
if generic_file.access_url:
return generic_file.access_url
# 否则,根据 storage_path 生成 URL
# TODO: 根据实际存储方式生成 URL本地存储、OSS 等)
# 这里暂时返回一个占位 URL
return f"/api/files/{file_id}/download"
async def _extract_document_text(self, file_id: uuid.UUID) -> str:
"""
提取文档文本内容
Args:
file_id: 文件ID
Returns:
str: 提取的文本内容
"""
generic_file = self.db.query(GenericFile).filter(
GenericFile.id == file_id,
GenericFile.status == "active"
).first()
if not generic_file:
raise BusinessException(
f"文件不存在或已删除: {file_id}",
BizCode.NOT_FOUND
)
# TODO: 根据文件类型提取文本
# - PDF: 使用 PyPDF2 或 pdfplumber
# - Word: 使用 python-docx
# - TXT/MD: 直接读取
file_ext = generic_file.file_ext.lower()
if file_ext in ['.txt', '.md', '.markdown']:
return await self._read_text_file(generic_file.storage_path)
elif file_ext == '.pdf':
return await self._extract_pdf_text(generic_file.storage_path)
elif file_ext in ['.doc', '.docx']:
return await self._extract_word_text(generic_file.storage_path)
else:
return f"[不支持的文档格式: {file_ext}]"
async def _read_text_file(self, storage_path: str) -> str:
"""读取纯文本文件"""
try:
with open(storage_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
logger.error(f"读取文本文件失败: {e}")
return f"[文件读取失败: {str(e)}]"
async def _extract_pdf_text(self, storage_path: str) -> str:
"""提取 PDF 文本"""
try:
# TODO: 实现 PDF 文本提取
# import PyPDF2 或 pdfplumber
return "[PDF 文本提取功能待实现]"
except Exception as e:
logger.error(f"提取 PDF 文本失败: {e}")
return f"[PDF 提取失败: {str(e)}]"
async def _extract_word_text(self, storage_path: str) -> str:
"""提取 Word 文档文本"""
try:
# TODO: 实现 Word 文本提取
# import docx
return "[Word 文本提取功能待实现]"
except Exception as e:
logger.error(f"提取 Word 文本失败: {e}")
return f"[Word 提取失败: {str(e)}]"
def get_multimodal_service(db: Session) -> MultimodalService:
"""获取多模态服务实例(依赖注入)"""
return MultimodalService(db)

View File

@@ -78,7 +78,8 @@ class OntologyService:
scenario: str,
domain: Optional[str] = None,
scene_id: Optional[Any] = None,
workspace_id: Optional[Any] = None
workspace_id: Optional[Any] = None,
language: str = "zh"
) -> OntologyExtractionResponse:
"""执行本体提取
@@ -91,6 +92,7 @@ class OntologyService:
domain: 可选的领域提示
scene_id: 可选的场景ID,用于权限验证(不再用于自动保存)
workspace_id: 可选的工作空间ID,用于权限验证
language: 输出语言 ("zh" 中文, "en" 英文)
Returns:
OntologyExtractionResponse: 提取结果
@@ -155,6 +157,7 @@ class OntologyService:
llm_max_tokens=self.DEFAULT_LLM_MAX_TOKENS,
max_description_length=self.DEFAULT_MAX_DESCRIPTION_LENGTH,
timeout=self.DEFAULT_LLM_TIMEOUT,
language=language,
)
extraction_duration = time.time() - extraction_start_time

View File

@@ -36,6 +36,7 @@ async def run_pilot_extraction(
dialogue_text: str,
db: Session,
progress_callback: Optional[Callable[[str, str, Optional[dict]], Awaitable[None]]] = None,
language: str = "zh",
) -> None:
"""
执行试运行模式的知识提取流水线。
@@ -43,10 +44,12 @@ async def run_pilot_extraction(
Args:
memory_config: 从数据库加载的内存配置对象
dialogue_text: 输入的对话文本
db: 数据库会话
progress_callback: 可选的进度回调函数
- 参数1 (stage): 当前处理阶段标识符
- 参数2 (message): 人类可读的进度消息
- 参数3 (data): 可选的附加数据字典
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
"""
log_file = "logs/time.log"
os.makedirs(os.path.dirname(log_file), exist_ok=True)
@@ -146,6 +149,7 @@ async def run_pilot_extraction(
config=config,
progress_callback=progress_callback,
embedding_id=str(memory_config.embedding_model_id),
language=language,
)
log_time("Orchestrator Initialization", time.time() - step_start, log_file)
@@ -191,6 +195,7 @@ async def run_pilot_extraction(
chunked_dialogs,
llm_client=llm_client,
embedder_client=embedder_client,
language=language,
)
log_time("Memory Summary Generation", time.time() - step_start, log_file)

View File

@@ -18,7 +18,7 @@ from app.repositories.end_user_repository import EndUserRepository
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
from app.schemas.memory_episodic_schema import EmotionSubject, EmotionType, type_mapping
from app.services.implicit_memory_service import ImplicitMemoryService
from app.services.memory_base_service import MemoryBaseService, MemoryTransService, Translation_English
from app.services.memory_base_service import MemoryBaseService, MemoryTransService
from app.services.memory_config_service import MemoryConfigService
from app.services.memory_perceptual_service import MemoryPerceptualService
from app.services.memory_short_service import ShortService
@@ -455,9 +455,7 @@ class UserMemoryService:
async def get_cached_memory_insight(
self,
db: Session,
end_user_id: str,
model_id: str,
language_type: str
end_user_id: str
) -> Dict[str, Any]:
"""
从数据库获取缓存的记忆洞察(四个维度)
@@ -465,7 +463,7 @@ class UserMemoryService:
Args:
db: 数据库会话
end_user_id: 终端用户ID (UUID)
Returns:
{
"memory_insight": str, # 总体概述
@@ -519,10 +517,6 @@ class UserMemoryService:
memory_insight=end_user.memory_insight
behavior_pattern=end_user.behavior_pattern
growth_trajectory=end_user.growth_trajectory
if language_type!='zh':
memory_insight=await Translation_English(model_id,memory_insight)
behavior_pattern=await Translation_English(model_id,behavior_pattern)
growth_trajectory=await Translation_English(model_id,growth_trajectory)
return {
"memory_insight":memory_insight, # 总体概述存储在 memory_insight
"behavior_pattern":behavior_pattern,
@@ -571,6 +565,8 @@ class UserMemoryService:
Args:
db: 数据库会话
end_user_id: 终端用户ID (UUID)
model_id: 模型ID用于翻译
language_type: 语言类型 ("zh" 中文, "en" 英文)
Returns:
{
@@ -604,11 +600,10 @@ class UserMemoryService:
personality_traits=end_user.personality_traits
core_values=end_user.core_values
one_sentence_summary=end_user.one_sentence_summary
if language_type!='zh':
user_summary=await Translation_English(model_id, user_summary)
personality_traits = await Translation_English(model_id, personality_traits)
core_values = await Translation_English(model_id, core_values)
one_sentence_summary = await Translation_English(model_id, one_sentence_summary)
# 直接返回数据库中的数据,不进行二次翻译
# 语言由生成时的 X-Language-Type 决定
has_cache = any([
user_summary,
personality_traits,
@@ -658,7 +653,8 @@ class UserMemoryService:
self,
db: Session,
end_user_id: str,
workspace_id: Optional[uuid.UUID] = None
workspace_id: Optional[uuid.UUID] = None,
language: str = "zh"
) -> Dict[str, Any]:
"""
生成并缓存记忆洞察
@@ -667,6 +663,7 @@ class UserMemoryService:
db: 数据库会话
end_user_id: 终端用户ID (UUID)
workspace_id: 工作空间ID (可选)
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
Returns:
{
@@ -679,7 +676,7 @@ class UserMemoryService:
}
"""
try:
logger.info(f"开始为 end_user_id {end_user_id} 生成记忆洞察")
logger.info(f"开始为 end_user_id {end_user_id} 生成记忆洞察, language={language}")
# 转换为UUID并查询用户
user_uuid = uuid.UUID(end_user_id)
@@ -700,7 +697,7 @@ class UserMemoryService:
# 使用 end_user_id 调用分析函数
try:
logger.info(f"使用 end_user_id={end_user_id} 生成记忆洞察")
result = await analytics_memory_insight_report(end_user_id)
result = await analytics_memory_insight_report(end_user_id, language=language)
memory_insight = result.get("memory_insight", "")
behavior_pattern = result.get("behavior_pattern", "")
@@ -789,7 +786,8 @@ class UserMemoryService:
self,
db: Session,
end_user_id: str,
workspace_id: Optional[uuid.UUID] = None
workspace_id: Optional[uuid.UUID] = None,
language: str = "zh"
) -> Dict[str, Any]:
"""
生成并缓存用户摘要(四个部分)
@@ -798,6 +796,7 @@ class UserMemoryService:
db: 数据库会话
end_user_id: 终端用户ID (UUID)
workspace_id: 工作空间ID (可选)
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
Returns:
{
@@ -810,7 +809,7 @@ class UserMemoryService:
}
"""
try:
logger.info(f"开始为 end_user_id {end_user_id} 生成用户摘要")
logger.info(f"开始为 end_user_id {end_user_id} 生成用户摘要, language={language}")
# 转换为UUID并查询用户
user_uuid = uuid.UUID(end_user_id)
@@ -831,7 +830,7 @@ class UserMemoryService:
# 使用 end_user_id 调用分析函数
try:
logger.info(f"使用 end_user_id={end_user_id} 生成用户摘要")
result = await analytics_user_summary(end_user_id)
result = await analytics_user_summary(end_user_id, language=language)
user_summary = result.get("user_summary", "")
personality = result.get("personality", "")
@@ -915,7 +914,8 @@ class UserMemoryService:
async def generate_cache_for_workspace(
self,
db: Session,
workspace_id: uuid.UUID
workspace_id: uuid.UUID,
language: str = "zh"
) -> Dict[str, Any]:
"""
为整个工作空间生成缓存
@@ -923,6 +923,7 @@ class UserMemoryService:
Args:
db: 数据库会话
workspace_id: 工作空间ID
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
Returns:
{
@@ -932,7 +933,7 @@ class UserMemoryService:
"errors": List[Dict]
}
"""
logger.info(f"开始为工作空间 {workspace_id} 批量生成缓存")
logger.info(f"开始为工作空间 {workspace_id} 批量生成缓存, language={language}")
total_users = 0
successful = 0
@@ -953,10 +954,10 @@ class UserMemoryService:
try:
# 生成记忆洞察
insight_result = await self.generate_and_cache_insight(db, end_user_id)
insight_result = await self.generate_and_cache_insight(db, end_user_id, language=language)
# 生成用户摘要
summary_result = await self.generate_and_cache_summary(db, end_user_id)
summary_result = await self.generate_and_cache_summary(db, end_user_id, language=language)
# 检查是否都成功
if insight_result["success"] and summary_result["success"]:
@@ -1007,7 +1008,7 @@ class UserMemoryService:
# 独立的分析函数
async def analytics_memory_insight_report(end_user_id: Optional[str] = None) -> Dict[str, Any]:
async def analytics_memory_insight_report(end_user_id: Optional[str] = None, language: str = "zh") -> Dict[str, Any]:
"""
生成记忆洞察报告(四个维度)
@@ -1019,6 +1020,7 @@ async def analytics_memory_insight_report(end_user_id: Optional[str] = None) ->
Args:
end_user_id: 可选的终端用户ID
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
Returns:
包含四个维度报告的字典: {
@@ -1029,8 +1031,12 @@ async def analytics_memory_insight_report(end_user_id: Optional[str] = None) ->
}
"""
from app.core.memory.utils.prompt.prompt_utils import render_memory_insight_prompt
from app.core.language_utils import validate_language
import re
# 验证语言参数
language = validate_language(language)
insight = MemoryInsightHelper(end_user_id)
try:
@@ -1070,7 +1076,8 @@ async def analytics_memory_insight_report(end_user_id: Optional[str] = None) ->
user_prompt = await render_memory_insight_prompt(
domain_distribution=domain_distribution_str,
active_periods=active_periods_str,
social_connections=social_connections_str
social_connections=social_connections_str,
language=language
)
messages = [
@@ -1097,11 +1104,11 @@ async def analytics_memory_insight_report(end_user_id: Optional[str] = None) ->
full_response = str(content) if content is not None else ""
# 7. 解析四个部分
# 使用正则表达式提取四个部分
memory_insight_match = re.search(r'【总体概述】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
behavior_match = re.search(r'【行为模式】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
findings_match = re.search(r'【关键发现】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
trajectory_match = re.search(r'【成长轨迹】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
# 使用正则表达式提取四个部分(支持中英文双语标题)
memory_insight_match = re.search(r'(?:总体概述|Overview)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
behavior_match = re.search(r'(?:行为模式|Behavior Pattern)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
findings_match = re.search(r'(?:关键发现|Key Findings)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
trajectory_match = re.search(r'(?:成长轨迹|Growth Trajectory)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
memory_insight = memory_insight_match.group(1).strip() if memory_insight_match else ""
behavior_pattern = behavior_match.group(1).strip() if behavior_match else ""
@@ -1128,7 +1135,7 @@ async def analytics_memory_insight_report(end_user_id: Optional[str] = None) ->
await insight.close()
async def analytics_user_summary(end_user_id: Optional[str] = None) -> Dict[str, Any]:
async def analytics_user_summary(end_user_id: Optional[str] = None, language: str = "zh") -> Dict[str, Any]:
"""
生成用户摘要(包含四个部分)
@@ -1139,6 +1146,7 @@ async def analytics_user_summary(end_user_id: Optional[str] = None) -> Dict[str,
Args:
end_user_id: 可选的终端用户ID
language: 语言类型 ("zh" 中文, "en" 英文),默认中文
Returns:
包含四部分摘要的字典: {
@@ -1149,8 +1157,12 @@ async def analytics_user_summary(end_user_id: Optional[str] = None) -> Dict[str,
}
"""
from app.core.memory.utils.prompt.prompt_utils import render_user_summary_prompt
from app.core.language_utils import validate_language
import re
# 验证语言参数
language = validate_language(language)
# 创建 UserSummaryHelper 实例
user_summary_tool = UserSummaryHelper(end_user_id or os.getenv("SELECTED_end_user_id", "group_123"))
@@ -1165,8 +1177,9 @@ async def analytics_user_summary(end_user_id: Optional[str] = None) -> Dict[str,
# 2) 使用 prompt_utils 渲染提示词
user_prompt = await render_user_summary_prompt(
user_id=user_summary_tool.user_id,
entities=", ".join(entity_lines) if entity_lines else "(空)",
statements=" | ".join(statement_samples) if statement_samples else "(空)"
entities=", ".join(entity_lines) if entity_lines else "(空)" if language == "zh" else "(empty)",
statements=" | ".join(statement_samples) if statement_samples else "(空)" if language == "zh" else "(empty)",
language=language
)
messages = [
@@ -1193,11 +1206,11 @@ async def analytics_user_summary(end_user_id: Optional[str] = None) -> Dict[str,
full_response = str(content) if content is not None else ""
# 5) 解析四个部分
# 使用正则表达式提取四个部分
user_summary_match = re.search(r'【基本介绍】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
personality_match = re.search(r'【性格特点】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
core_values_match = re.search(r'【核心价值观】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
one_sentence_match = re.search(r'【一句话总结】\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
# 使用正则表达式提取四个部分(支持中英文标题)
user_summary_match = re.search(r'(?:基本介绍|Basic Introduction)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
personality_match = re.search(r'(?:性格特点|Personality Traits)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
core_values_match = re.search(r'(?:核心价值观|Core Values)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
one_sentence_match = re.search(r'(?:一句话总结|One-Sentence Summary)\s*\n(.*?)(?=\n【|$)', full_response, re.DOTALL)
user_summary = user_summary_match.group(1).strip() if user_summary_match else ""
personality = personality_match.group(1).strip() if personality_match else ""

View File

@@ -481,13 +481,16 @@ def read_message_task(self, end_user_id: str, message: str, history: List[Dict[s
@celery_app.task(name="app.core.memory.agent.write_message", bind=True)
def write_message_task(self, end_user_id: str, message: str, config_id: str, storage_type:str, user_rag_memory_id:str) -> Dict[str, Any]:
def write_message_task(self, end_user_id: str, message: str, config_id: str, storage_type:str, user_rag_memory_id:str, language: str = "zh") -> Dict[str, Any]:
"""Celery task to process a write message via MemoryAgentService.
Args:
end_user_id: Group ID for the memory agent (also used as end_user_id)
message: Message to write
config_id: Configuration ID as string (will be converted to UUID)
storage_type: Storage type (neo4j or rag)
user_rag_memory_id: User RAG memory ID
language: 语言类型 ("zh" 中文, "en" 英文)
Returns:
Dict containing the result and metadata
@@ -498,7 +501,7 @@ def write_message_task(self, end_user_id: str, message: str, config_id: str, sto
from app.core.logging_config import get_logger
logger = get_logger(__name__)
logger.info(f"[CELERY WRITE] Starting write task - end_user_id={end_user_id}, config_id={config_id}, storage_type={storage_type}")
logger.info(f"[CELERY WRITE] Starting write task - end_user_id={end_user_id}, config_id={config_id}, storage_type={storage_type}, language={language}")
start_time = time.time()
# Convert config_id string to UUID
@@ -535,9 +538,9 @@ def write_message_task(self, end_user_id: str, message: str, config_id: str, sto
async def _run() -> str:
db = next(get_db())
try:
logger.info(f"[CELERY WRITE] Executing MemoryAgentService.write_memory with config_id={actual_config_id} (type: {type(actual_config_id).__name__})")
logger.info(f"[CELERY WRITE] Executing MemoryAgentService.write_memory with config_id={actual_config_id} (type: {type(actual_config_id).__name__}), language={language}")
service = MemoryAgentService()
result = await service.write_memory(end_user_id, message, actual_config_id, db, storage_type, user_rag_memory_id)
result = await service.write_memory(end_user_id, message, actual_config_id, db, storage_type, user_rag_memory_id, language)
logger.info(f"[CELERY WRITE] Write completed successfully: {result}")
return result
except Exception as e:

View File

@@ -1,9 +1,4 @@
# Language Configuration
# Supported values: "zh" (Chinese), "en" (English)
# This controls the language used for memory summary titles and other generated content
DEFAULT_LANGUAGE=zh
# Neo4j Configuration (记忆系统数据库)
NEO4J_URI=
NEO4J_USERNAME=

3082
api/uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,39 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 13:59:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 13:59:41
*/
import { request } from '@/utils/request'
import type { ApiKey } from '@/views/ApiKeyManagement/types'
// API Key列表
// API Key list
export const getApiKeyListUrl = '/apikeys'
export const getApiKeyList = (data: Record<string, unknown>) => {
return request.get(getApiKeyListUrl, data)
}
// API Key详情
// API Key details
export const getApiKey = (id: string) => {
return request.get(`/apikeys/${id}`)
}
// 创建API Key
// Create API Key
export const createApiKey = (values: ApiKey) => {
return request.post('/apikeys', values)
}
// 更新API Key
// Update API Key
export const updateApiKey = (id: string, values: ApiKey) => {
return request.put(`/apikeys/${id}`, values)
}
// 删除 API Key
// Delete API Key
export const deleteApiKey = (id: string) => {
return request.delete(`/apikeys/${id}`)
}
// 使用统计
// Usage statistics
export const getApiKeyStats = (app_key_id: string) => {
return request.get(`/apikeys/${app_key_id}/stats`)
}

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 13:59:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 13:59:45
*/
import { request } from '@/utils/request'
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
import type { Config } from '@/views/ApplicationConfig/types'
@@ -5,71 +11,72 @@ import { handleSSE, type SSEMessage } from '@/utils/stream'
import type { QueryParams } from '@/views/Conversation/types'
import type { WorkflowConfig } from '@/views/Workflow/types'
// 应用列表
// Application list
export const getApplicationListUrl = '/apps'
export const getApplicationList = (data: Record<string, unknown>) => {
return request.get(getApplicationListUrl, data)
}
// 获取应用配置
// Get application config
export const getApplicationConfig = (id: string) => {
return request.get(`/apps/${id}/config`)
}
// 获取集群应用配置
// Get multi-agent config
export const getMultiAgentConfig = (id: string) => {
return request.get(`/apps/${id}/multi-agent`)
}
// 获取 workflow应用配置
// Get workflow config
export const getWorkflowConfig = (id: string) => {
return request.get(`/apps/${id}/workflow`)
}
// 应用详情
// Application details
export const getApplication = (id: string) => {
return request.get(`/apps/${id}`)
}
// 更新应用
// Update application
export const updateApplication = (id: string, values: ApplicationModalData) => {
return request.put(`/apps/${id}`, values)
}
// 创建应用
// Create application
export const addApplication = (values: ApplicationModalData) => {
return request.post('/apps', values)
}
// 保存Agent配置
// Save agent config
export const saveAgentConfig = (app_id: string, values: Config) => {
return request.put(`/apps/${app_id}/config`, values)
}
// 保存集群配置
// Save multi-agent config
export const saveMultiAgentConfig = (app_id: string, values: Config) => {
return request.put(`/apps/${app_id}/multi-agent`, values)
}
// 保存workflow配置
// Save workflow config
export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => {
return request.put(`/apps/${app_id}/workflow`, values)
}
// 模型比对试运行
// Model comparison test run
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage)
}
// Test run
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage)
}
// 删除应用
// Delete application
export const deleteApplication = (app_id: string) => {
return request.delete(`/apps/${app_id}`)
}
// 发布版本列表
// Release version list
export const getReleaseList = (app_id: string) => {
return request.get(`/apps/${app_id}/releases`)
}
// 发布版本
// Publish release
export const publishRelease = (app_id: string, values: Record<string, unknown>) => {
return request.post(`/apps/${app_id}/publish`, values)
}
// 回滚版本
// Rollback release
export const rollbackRelease = (app_id: string, version: string) => {
return request.post(`/apps/${app_id}/rollback/${version}`)
}
// 发布版本分享
// Share release
export const shareRelease = (app_id: string, release_id: string) => {
return request.post(`/apps/${app_id}/releases/${release_id}/share`, {
"is_enabled": true,
@@ -77,7 +84,7 @@ export const shareRelease = (app_id: string, release_id: string) => {
"allow_embed": true
})
}
// 获取体验对话历史
// Get conversation history
export const getConversationHistory = (share_token: string, data: { page: number; pagesize: number }) => {
return request.get(`/public/share/conversations`, data, {
headers: {
@@ -85,7 +92,7 @@ export const getConversationHistory = (share_token: string, data: { page: number
}
})
}
// 发送体验对话
// Send conversation
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => {
return handleSSE(`/public/share/chat`, values, onMessage, {
headers: {
@@ -93,7 +100,7 @@ export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessa
}
})
}
// 获取体验会话详情
// Get conversation details
export const getConversationDetail = (share_token: string, conversation_id: string) => {
return request.get(`/public/share/conversations/${conversation_id}`, {}, {
headers: {
@@ -101,15 +108,15 @@ export const getConversationDetail = (share_token: string, conversation_id: stri
}
})
}
// 获取体验对话token
// Get share token
export const getShareToken = (share_token: string, user_id: string) => {
return request.post(`/public/share/${share_token}/token`, { user_id })
}
// 复制应用
// Copy application
export const copyApplication = (app_id: string, new_name: string) => {
return request.post(`/apps/${app_id}/copy?new_name=${new_name}`)
}
// 数据统计
// Data statistics
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
return request.get(`/apps/${app_id}/statistics`, data)
}

View File

@@ -1,5 +1,5 @@
import { request } from "@/utils/request";
// 列表查询参数
// List query parameters
export interface Query {
page?: number;
pagesize?: number;
@@ -38,15 +38,15 @@ export interface versionResponse{
codeName: string;
};
}
// 首页数据统计
// Dashboard data statistics
export const getDashboardData = `/home-page/workspaces`
// 首页数据看板统计
// Dashboard statistics
export const getDashboardStatistics = async () => {
const response = await request.get(`/home-page/statistics`);
return response as DataResponse;
};
// 获取版本号
// Get version
export const getVersion = async () => {
const response = await request.get(`/home-page/version`);
return response as versionResponse;

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 13:59:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 13:59:56
*/
import { request, API_PREFIX } from '@/utils/request'
// Upload filefile storage has expiration period

View File

@@ -1,20 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:01
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:01
*/
import { request } from '@/utils/request'
// 成员列表
// Member list
export const memberListUrl = '/workspaces/members'
// 邀请成员
// Invite member
export const inviteMember = (values: { email: string }) => {
return request.post(`/workspaces/invites`, values)
}
// 删除成员
// Delete member
export const deleteMember = (id: string) => {
return request.delete(`/workspaces/members/${id}`)
}
// 更新成员
// Update member
export const updateMember = (values: { id: string, role: string }) => {
return request.put(`/workspaces/members`, [values])
}
// 验证邀请token
// Validate invite token
export const validateInviteToken = (token: string) => {
return request.get(`/workspaces/invites/validate/${token}`)
}

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:06
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:06
*/
import { request } from '@/utils/request'
import type {
MemoryFormData,

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:09
*/
import { request } from '@/utils/request'
import type { MultiKeyForm, Query, KeyConfigModalForm, CompositeModelForm, CustomModelForm } from '@/views/ModelManagement/types'

View File

@@ -1,5 +1,11 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 13:59:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 13:59:12
*/
import { request } from '@/utils/request'
import type { Query, OntologyModalData, OntologyClassModalData, OntologyClassExtractModalData } from '@/views/Ontology/types'
import type { Query, OntologyModalData, OntologyClassModalData, OntologyClassExtractModalData, OntologyExportModalData } from '@/views/Ontology/types'
// Scene list
export const getOntologyScenesUrl = '/memory/ontology/scenes'
@@ -37,3 +43,11 @@ export const createOntologyClass = (data: OntologyClassModalData) => {
export const deleteOntologyClass = (class_id: string) => {
return request.delete(`/memory/ontology/class/${class_id}`)
}
// Import scenario
export const ontologyImport = (data: unknown) => {
return request.uploadFile('/memory/ontology/import', data)
}
// Export scenario
export const ontologyExport = (data: OntologyExportModalData, fileName: string, callback: () => void) => {
return request.downloadFile('/memory/ontology/export', fileName, data, callback)
}

View File

@@ -1,16 +1,23 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:14
*/
import { request } from '@/utils/request'
import type { VoucherForm } from '@/views/OrderPayment/types'
export const getOrderListUrl = '/v1/orders/customer'
// 提交支付凭证API
// Submit payment voucher API
export const submitPaymentVoucherAPI = (voucherData: VoucherForm) => {
return request.post('/v1/orders/', voucherData)
}
// 订单详情
// Order details
export const getOrderDetail = (order_no: string) => {
return request.get(`/v1/orders/customer/${order_no}`)
}
// Order status enum
export const orderStatusUrl = '/v1/order-status/'
export const getOrderStatus = () => {
return request.get(orderStatusUrl)

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:17
*/
import { request } from '@/utils/request'
import type { AiPromptForm } from '@/views/ApplicationConfig/types'
import type { PromptReleaseData } from '@/views/Prompt/types'

View File

@@ -1,40 +1,46 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:23
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:23
*/
import { request } from '@/utils/request'
import type { CreateModalData } from '@/views/UserManagement/types'
import { cookieUtils } from '@/utils/request'
// 用户信息
// User info
export const getUsers = () => {
return request.get('/users')
}
// 用户列表
// User list
export const getUserListUrl = '/users/superusers'
// 登录
// Login
export const loginUrl = '/token'
export const login = (data: { email: string; password: string; invite?: string; username?: string }) => {
return request.post(loginUrl, data)
}
// 刷新token
// Refresh token
export const refreshTokenUrl = '/refresh'
export const refreshToken = () => {
return request.post(refreshTokenUrl, { refresh_token: cookieUtils.get('refreshToken') })
}
// 重置密码
// Reset password
export const changePassword = (data: { user_id: string; new_password: string }) => {
return request.put('/users/admin/change-password', data)
}
// 禁用用户
// Disable user
export const deleteUser = (user_id: string) => {
return request.delete(`/users/${user_id}`)
}
// 启用用户
// Enable user
export const enableUser = (user_id: string) => {
return request.post(`/users/${user_id}/activate`)
}
// 创建用户
// Create user
export const addUser = (data: CreateModalData) => {
return request.post('/users/superuser', data)
}
// 注销
// Logout
export const logoutUrl = '/logout'
export const logout = () => {
return request.post(logoutUrl)

View File

@@ -1,28 +1,34 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:00:26
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:00:26
*/
import { request } from '@/utils/request'
import type { SpaceModalData } from '@/views/SpaceManagement/types'
import type { ConfigModalData } from '@/views/UserMemory/types'
import type { SpaceConfigData } from '@/views/SpaceConfig/types'
// 空间列表
// Workspace list
export const getWorkspaces = () => {
return request.get('/workspaces')
}
// 创建空间
// Create workspace
export const createWorkspace = (values: SpaceModalData) => {
return request.post('/workspaces', values)
}
// 切换空间
// Switch workspace
export const switchWorkspace = (workspaceId: string) => {
return request.put(`/workspaces/${workspaceId}/switch`)
}
// 获取空间存储类型
// Get workspace storage type
export const getWorkspaceStorageType = () => {
return request.get(`/workspaces/storage`)
}
// 获取空间模型配置
// Get workspace model config
export const getWorkspaceModels = () => {
return request.get(`/workspaces/workspace_models`)
}
// 更新空间模型配置
export const updateWorkspaceModels = (data: ConfigModalData) => {
// Update workspace model config
export const updateWorkspaceModels = (data: SpaceConfigData) => {
return request.put(`/workspaces/workspace_models`, data)
}

View File

@@ -1,14 +1,36 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:01:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:46:05
*/
/**
* ButtonCheckbox - A custom checkbox component styled as a button
*
* This component provides a button-like interface for checkbox functionality,
* with support for custom icons and visual states (checked/unchecked).
*
* @component
*/
import { type FC, type ReactNode, useEffect } from 'react';
import { type RadioGroupProps } from 'antd';
import clsx from 'clsx'
// Button checkbox component props
interface ButtonCheckboxProps extends Omit<RadioGroupProps, 'onChange'> {
/** Whether the checkbox is checked */
checked?: boolean;
/** Callback fired when value changes (for side effects) */
onValueChange?: (checked: boolean) => void;
/** Callback fired when checkbox state changes */
onChange?: (checked: boolean) => void;
/** Icon path for unchecked state */
icon?: string;
/** Icon path for checked state */
checkedIcon?: string;
/** Button content */
children?: ReactNode
}
@@ -20,13 +42,14 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
checkedIcon,
children,
}) => {
// 监听value变化
// Listen to value changes and trigger side effects via onValueChange callback
useEffect(() => {
if (onValueChange) {
onValueChange(checked);
}
}, [checked, onValueChange]);
// Toggle checked state when button is clicked
const handleChange = () => {
if (onChange) {
onChange(!checked);
@@ -34,11 +57,18 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
}
return (
<div className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": checked,
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
})} onClick={handleChange}>
<div
className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
// Checked state: blue background and border
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": checked,
// Unchecked state: gray border and dark text
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
})}
onClick={handleChange}
>
{/* Display unchecked icon when not checked */}
{icon && !checked && <img src={icon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{/* Display checked icon when checked */}
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{children}
</div>

View File

@@ -1,31 +1,61 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:02:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:46:29
*/
/**
* CustomSelect - A select component that fetches options from an API
*
* This component extends Ant Design's Select with automatic data fetching,
* search functionality, and customizable option formatting.
*
* @component
*/
import { useEffect, useState, useMemo, type FC, type Key } from 'react';
import { Select } from 'antd';
import type { SelectProps, DefaultOptionType } from 'antd/es/select';
import { useTranslation } from 'react-i18next';
import { request } from '@/utils/request';
// Generic option type for API response data
interface OptionType {
[key: string]: Key | string | number;
}
// API response structure
interface ApiResponse<T> {
items?: T[];
}
interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
/** API endpoint URL to fetch options */
url: string;
/** Query parameters for the API request */
params?: Record<string, unknown>;
/** Key name for option value in response data */
valueKey?: string;
/** Key name for option label in response data */
labelKey?: string;
/** Placeholder text for the select */
placeholder?: string;
/** Whether to show "All" option */
hasAll?: boolean;
/** Custom text for "All" option */
allTitle?: string;
/** Function to format/transform the options data */
format?: (items: OptionType[]) => OptionType[];
/** Whether to enable search functionality */
showSearch?: boolean;
/** Property name to filter options by */
optionFilterProp?: string;
/** Custom filter function for search */
filterOption?: (inputValue: string, option?: DefaultOptionType) => boolean;
}
// Default filter function for search - performs case-insensitive substring matching
const defaultFilterOption = (inputValue: string, option?: DefaultOptionType): boolean => {
if (!option || !inputValue) return true;
const label = String(option.children || option.label || '');
@@ -47,8 +77,10 @@ const CustomSelect: FC<CustomSelectProps> = ({
}) => {
const { t } = useTranslation();
const [options, setOptions] = useState<OptionType[]>([]);
// Memoize params to prevent unnecessary re-fetches
const memoizedParams = useMemo(() => params, [JSON.stringify(params)]);
// Fetch options from API when url or params change
useEffect(() => {
request.get<ApiResponse<OptionType>>(url, memoizedParams).then((res) => {
const data = Array.isArray(res) ? res : res?.items || [];
@@ -56,6 +88,7 @@ const CustomSelect: FC<CustomSelectProps> = ({
});
}, [url, memoizedParams]);
// Apply custom format function if provided
const displayOptions = format ? format(options) : options;
return (
@@ -66,7 +99,9 @@ const CustomSelect: FC<CustomSelectProps> = ({
filterOption={filterOption || defaultFilterOption}
{...props}
>
{/* Optional "All" option for selecting all items */}
{hasAll && <Select.Option value={null}>{allTitle || t('common.all')}</Select.Option>}
{/* Render options from API data */}
{displayOptions.map((option) => (
<Select.Option key={option[valueKey]} value={option[valueKey]}>
{String(option[labelKey])}

View File

@@ -1,19 +1,41 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:02:47
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:47:24
*/
/**
* BodyWrapper Component
*
* A wrapper component that conditionally renders loading, empty, or content states.
* Simplifies state management for data-driven components.
*
* @component
*/
import type { FC, ReactNode } from 'react'
import PageEmpty from './PageEmpty'
import PageLoading from './PageLoading'
interface BodyWrapperProps {
/** Content to render when not loading or empty */
children: ReactNode
/** Whether to show loading state */
loading?: boolean
/** Whether the content is empty */
empty: boolean
}
const BodyWrapper: FC<BodyWrapperProps> = ({ children, loading = false, empty }) => {
// Show loading spinner while data is being fetched
if (loading) {
return <PageLoading />
}
// Show empty state when no data is available
if (!loading && empty) {
return <PageEmpty />
}
// Render actual content when data is loaded and available
return children
}
export default BodyWrapper

View File

@@ -1,7 +1,28 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:03:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:48:41
*/
/**
* Loading Component
*
* A specialized empty state component that displays a loading animation.
* Uses the Empty component with a loading icon and localized loading messages.
*
* @component
*/
import { type FC } from 'react';
import { useTranslation } from 'react-i18next'
import LoadingIcon from '@/assets/images/loading.svg'
import Empty from './index'
const Loading = ({ size = 200 }: { size?: number }) => {
/**
* @param size - Icon size in pixels (default: 200)
*/
const Loading: FC<{ size?: number }> = ({ size = 200 }) => {
const { t } = useTranslation()
return (
<Empty

View File

@@ -1,7 +1,28 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:04:18
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:49:01
*/
/**
* PageEmpty Component
*
* A full-page empty state component that displays when no content is available.
* Uses the Empty component with a page-specific empty icon and messages.
*
* @component
*/
import { type FC } from 'react';
import { useTranslation } from 'react-i18next'
import pageEmptyIcon from '@/assets/images/empty/pageEmpty.png'
import Empty from './index'
const PageEmpty = ({ size = [240, 210] }: { size?: number | number[] }) => {
/**
* @param size - Icon size in pixels - single number or [width, height] array (default: [240, 210])
*/
const PageEmpty: FC<{ size?: number | number[] }> = ({ size = [240, 210] }) => {
const { t } = useTranslation()
return (
<Empty

View File

@@ -1,7 +1,28 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:04:43
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:49:49
*/
/**
* PageLoading Component
*
* A full-page loading state component that displays while content is being fetched.
* Uses the Empty component with a loading icon and localized loading messages.
*
* @component
*/
import { type FC } from 'react';
import { useTranslation } from 'react-i18next'
import LoadingIcon from '@/assets/images/empty/pageLoading.png'
import Empty from './index'
const PageLoading = ({ size = [240, 210] }: { size?: number | number[] }) => {
/**
* @param size - Icon size in pixels - single number or [width, height] array (default: [240, 210])
*/
const PageLoading: FC<{ size?: number | number[] }> = ({ size = [240, 210] }) => {
const { t } = useTranslation()
return (
<Empty

View File

@@ -1,13 +1,35 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:03:25
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:47:31
*/
/**
* Empty Component
*
* A customizable empty state component that displays an icon with optional title and subtitle.
* Used to indicate when no data or content is available.
*
* @component
*/
import { type FC } from 'react';
import { useTranslation } from 'react-i18next';
import emptyIcon from '@/assets/images/empty/empty.svg';
interface EmptyProps {
/** Custom icon URL for the empty state */
url?: string;
/** Icon size - single number or [width, height] array */
size?: number | number[];
/** Main title text */
title?: string;
/** Whether to show subtitle */
isNeedSubTitle?: boolean;
/** Custom subtitle text */
subTitle?: string;
/** Additional CSS classes */
className?: string;
}
const Empty: FC<EmptyProps> = ({
@@ -19,14 +41,19 @@ const Empty: FC<EmptyProps> = ({
className = '',
}) => {
const { t } = useTranslation();
// Calculate width and height from size prop (supports single value or [width, height] array)
const width = Array.isArray(size) ? size[0] : size ? size : url ? 200 : 88;
const height = Array.isArray(size) ? size[1] : size ? size : url ? 200 : 88;
// Use custom subtitle or default translation if subtitle is needed
const curSubTitle = isNeedSubTitle ? (subTitle || t('empty.tableEmpty')) : null;
return (
<div className={`rb:flex rb:items-center rb:justify-center rb:flex-col ${className}`}>
{/* Empty state icon */}
<img src={url || emptyIcon} alt="404" style={{ width: `${width}px`, height: `${height}px` }} />
{/* Optional title */}
{title && <div className="rb:mt-2 rb:leading-5">{title}</div>}
{/* Optional subtitle with conditional styling */}
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{curSubTitle}</div>}
</div>
);

View File

@@ -1,6 +1,25 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:05:16
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:05:16
*/
/**
* DescWrapper Component
*
* A styled wrapper for displaying description text in forms.
* Provides consistent typography and styling for form field descriptions.
*
* @component
*/
import clsx from "clsx";
import type { FC, ReactNode } from "react";
/**
* @param desc - Description content (string or React node)
* @param className - Additional CSS classes for customization
*/
const DescWrapper: FC<{desc: string | ReactNode, className?: string}> = ({desc, className}) => {
return (
<div className={clsx(className, "rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>

View File

@@ -1,9 +1,30 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:05:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:05:41
*/
/**
* LabelWrapper Component
*
* A styled wrapper for displaying form field labels with optional child content.
* Provides consistent typography and layout for form labels.
*
* @component
*/
import clsx from "clsx";
import type { FC, ReactNode } from "react";
/**
* @param title - Label text or React node to display
* @param className - Additional CSS classes for customization
* @param children - Optional child content to render below the label
*/
const LabelWrapper: FC<{ title: string | ReactNode, className?: string; children?: ReactNode}> = ({title, className, children}) => {
return (
<div className={clsx(className)}>
{/* Label title with consistent styling */}
<div className="rb:text-[14px] rb:font-medium rb:leading-5">{title}</div>
{children}
</div>

View File

@@ -1,17 +1,36 @@
import { Switch, Form, ConfigProvider } from "antd";
import useSize from 'antd/lib/config-provider/hooks/useSize'
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:06:24
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:50:49
*/
/**
* SwitchFormItem Component
*
* A form item component that combines a switch control with a label and optional description.
* Provides a consistent layout for switch-based form fields.
*
* @component
*/
import { Switch, Form } from "antd";
import type { FC, ReactNode } from "react";
import { useContext } from "react";
import LabelWrapper from './LabelWrapper'
import DescWrapper from './DescWrapper'
interface SwitchFormItemProps {
/** Label text or React node */
title: string | ReactNode;
/** Optional description text or React node */
desc?: string | ReactNode;
/** Form field name (string or nested path array) */
name: string | string[];
/** Switch size */
size?: 'small' | 'default'
/** Additional CSS classes */
className?: string;
/** Whether the switch is disabled */
disabled?: boolean;
}
@@ -23,14 +42,13 @@ const SwitchFormItem: FC<SwitchFormItemProps> = ({
className,
disabled
}) => {
const componentSize = useSize()
console.log('componentSize', componentSize)
return (
<div className={`${className} rb:flex rb:items-center rb:justify-between`}>
{/* Label and description section */}
<LabelWrapper title={title}>
{desc && <DescWrapper desc={desc} className="rb:mt-2" />}
</LabelWrapper>
{/* Switch control */}
<Form.Item
name={name}
valuePropName="checked"

View File

@@ -1,3 +1,18 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:08:58
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:08:58
*/
/**
* SettingModal Component
*
* A modal dialog for configuring application settings including language and timezone.
* Uses forwardRef to expose open/close methods to parent components.
*
* @component
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Select } from 'antd';
import { useTranslation } from 'react-i18next';
@@ -7,34 +22,39 @@ import { useI18n } from '@/store/locale'
import { timezones } from '@/utils/timezones'
const FormItem = Form.Item;
/** Interface for SettingModal ref methods exposed to parent components */
export interface SettingModalRef {
/** Open the settings modal */
handleOpen: () => void;
/** Close the settings modal */
handleClose: () => void;
}
/** Settings modal component for language and timezone configuration */
const SettingModal = forwardRef<SettingModalRef>((_props, ref) => {
const { t } = useTranslation();
const { changeLanguage, language, timeZone, changeTimeZone } = useI18n()
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑
/** Close modal and reset form to initial state */
const handleClose = () => {
setVisible(false);
form.resetFields();
};
/** Open modal and populate form with current settings */
const handleOpen = () => {
form.setFieldsValue({ language, timeZone })
setVisible(true);
};
// 封装保存方法,添加提交逻辑
/** Validate and save settings, update language and timezone if changed */
const handleSave = () => {
form
.validateFields()
.then(() => {
.then((values) => {
const { language: newLanguage, timeZone: newTimeZone } = values
if (newLanguage !== language) {
changeLanguage(newLanguage);
@@ -47,11 +67,12 @@ const SettingModal = forwardRef<SettingModalRef>((_props, ref) => {
});
}
// 暴露给父组件的方法
/** Expose handleOpen and handleClose methods to parent component via ref */
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t('header.setting')}
@@ -64,7 +85,7 @@ const SettingModal = forwardRef<SettingModalRef>((_props, ref) => {
form={form}
layout="vertical"
>
{/* 中英文切换 */}
{/* Language selection dropdown */}
<FormItem
name="language"
label={t('header.language')}
@@ -73,7 +94,7 @@ const SettingModal = forwardRef<SettingModalRef>((_props, ref) => {
options={['zh', 'en'].map(key => ({ label: t(`header.${key}`), value: key }))}
/>
</FormItem>
{/* 时区切换 */}
{/* Timezone selection dropdown */}
<FormItem
name="timeZone"
label={t('header.timeZone')}

View File

@@ -1,39 +1,61 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:09:47
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:51:54
*/
/**
* UserInfoModal Component
*
* A modal dialog that displays user profile information and security settings.
* Includes basic user details and password change functionality.
* Uses forwardRef to expose open/close methods to parent components.
*
* @component
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Button } from 'antd';
import { UnlockOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useUser } from '@/store/user';
import { useUser } from '@/store/user';
import RbModal from '@/components/RbModal'
import { formatDateTime } from '@/utils/format';
import ResetPasswordModal from '@/views/UserManagement/components/ResetPasswordModal'
import type { ResetPasswordModalRef } from '@/views/UserManagement/types'
/** Interface for UserInfoModal ref methods exposed to parent components */
export interface UserInfoModalRef {
/** Open the user info modal */
handleOpen: () => void;
/** Close the user info modal */
handleClose: () => void;
}
/** User information modal component displaying user details and security settings */
const UserInfoModal = forwardRef<UserInfoModalRef>((_props, ref) => {
const { t } = useTranslation();
const resetPasswordModalRef = useRef<ResetPasswordModalRef>(null)
const { user } = useUser();
const [visible, setVisible] = useState(false);
// 封装取消方法,添加关闭弹窗逻辑
/** Close the modal */
const handleClose = () => {
setVisible(false);
};
/** Open the modal */
const handleOpen = () => {
setVisible(true);
};
// 暴露给父组件的方法
/** Expose handleOpen and handleClose methods to parent component via ref */
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t('header.userInfo')}
@@ -41,32 +63,40 @@ const UserInfoModal = forwardRef<UserInfoModalRef>((_props, ref) => {
onCancel={handleClose}
footer={null}
>
{/* Basic Information Section */}
<div className="rb:text-[#5B6167] rb:font-medium">{t('header.basicInfo')}</div>
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px] rb:mt-[12px]">
{/* Username */}
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3 rb:mt-3">
<span className="rb:whitespace-nowrap">{t('user.username')}</span>
<span className="rb:text-[#212332]">{user.username}</span>
</div>
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]">
{/* Email */}
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3">
<span className="rb:whitespace-nowrap">{t('user.email')}</span>
<span className="rb:text-[#212332]">{user.email}</span>
</div>
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]">
{/* Role */}
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3">
<span className="rb:whitespace-nowrap">{t('user.role')}</span>
<span className="rb:text-[#212332]">{user.is_superuser ? t('user.superuser') : t('user.normalUser')}</span>
</div>
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]">
{/* Created Date */}
<div className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3">
<span className="rb:whitespace-nowrap">{t('user.createdAt')}</span>
<span className="rb:text-[#212332]">{formatDateTime(user.created_at, 'YYYY-MM-DD HH:mm:ss')}</span>
</div>
<div className="rb:text-[#5B6167] rb:font-medium rb:mt-[24px]">{t('header.securitySettings')}</div>
{/* Security Settings Section */}
<div className="rb:text-[#5B6167] rb:font-medium rb:mt-6">{t('header.securitySettings')}</div>
<div className="rb:mt-[12px] rb:bg-[#F0F3F8] rb:p-[10px_12px] rb:rounded-[6px] rb:flex rb:items-center rb:justify-between rb:gap-[8px]">
<div className="rb:flex rb:items-center rb:gap-[12px]">
{/* Password Change Card */}
<div className="rb:mt-3 rb:bg-[#F0F3F8] rb:p-[10px_12px] rb:rounded-md rb:flex rb:items-center rb:justify-between rb:gap-2">
<div className="rb:flex rb:items-center rb:gap-3">
<UnlockOutlined className="rb:text-[24px]" />
<div>
<div className="rb:leading-[20px]">{t('header.changePassword')}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-[4px] rb:leading-[16px]">{t('header.changePasswordDesc')}</div>
<div className="rb:leading-5">{t('header.changePassword')}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1 rb:leading-4">{t('header.changePasswordDesc')}</div>
</div>
</div>
<Button onClick={() => resetPasswordModalRef.current?.handleOpen(user)}>{t('common.change')}</Button>

View File

@@ -1,16 +1,36 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:07:49
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:07:49
*/
/**
* AppHeader Component
*
* The main application header that displays breadcrumb navigation and user menu.
* Supports different breadcrumb sources based on the current route.
*
* @component
*/
import { type FC, useRef } from 'react';
import { Layout, Dropdown, Space, Breadcrumb } from 'antd';
import { Layout, Dropdown, Breadcrumb } from 'antd';
import type { MenuProps, BreadcrumbProps } from 'antd';
import { UserOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useUser } from '@/store/user';
import { useMenu } from '@/store/menu';
import styles from './index.module.css'
import SettingModal, { type SettingModalRef } from './SettingModal'
import UserInfoModal, { type UserInfoModalRef } from './UserInfoModal'
const { Header } = Layout;
/**
* @param source - Breadcrumb source type ('space' or 'manage'), defaults to 'manage'
*/
const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
const { t } = useTranslation();
const location = useLocation();
@@ -20,21 +40,26 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
const { user, logout } = useUser();
const { allBreadcrumbs } = useMenu();
// 根据当前路由动态选择面包屑源
/**
* Dynamically select breadcrumb source based on current route
* - Knowledge base list: uses 'space' breadcrumb
* - Knowledge base detail: uses 'space-detail' breadcrumb
* - Other pages: uses the passed source prop
*/
const getBreadcrumbSource = () => {
const pathname = location.pathname;
// 知识库列表页面使用默认的 space 面包屑
// Knowledge base list page uses default space breadcrumb
if (pathname === '/knowledge-base') {
return 'space';
}
// 知识库详情相关页面使用独立的面包屑
// Knowledge base detail pages use independent breadcrumb
if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') {
return 'space-detail';
}
// 其他页面使用传入的 source
// Other pages use the passed source
return source;
};
@@ -42,13 +67,12 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
const breadcrumbs = allBreadcrumbs[breadcrumbSource] || [];
// 处理退出登录
/** Handle user logout */
const handleLogout = () => {
logout()
};
// 用户下拉菜单配置
/** User dropdown menu configuration with profile, settings, and logout options */
const userMenuItems: MenuProps['items'] = [
{
key: '1',
@@ -89,18 +113,25 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
onClick: handleLogout,
},
];
/**
* Format breadcrumb items with proper titles, paths, and click handlers
* - Translates i18n keys to display text
* - Handles custom onClick events
* - Disables navigation for the last breadcrumb item
*/
const formatBreadcrumbNames = () => {
return breadcrumbs.map((menu, index) => {
const item: any = {
title: menu.i18nKey ? t(menu.i18nKey) : menu.label,
};
// 如果是最后一项,不设置 path
// If it's the last item, don't set path
if (index === breadcrumbs.length - 1) {
return item;
}
// 如果有自定义 onClick,使用 onClick 并设置 href '#' 以显示手型光标
// If has custom onClick, use onClick and set href to '#' to show pointer cursor
if ((menu as any).onClick) {
item.onClick = (e: React.MouseEvent) => {
e.preventDefault();
@@ -108,35 +139,26 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
};
item.href = '#';
} else if (menu.path && menu.path !== '#') {
// 只有当 path 不是 '#' 时才设置 path
// Only set path when path is not '#'
item.path = menu.path;
}
return item;
});
}
return (
<Header className={styles.header}>
{/* Breadcrumb navigation */}
<Breadcrumb separator=">" items={formatBreadcrumbNames() as BreadcrumbProps['items']} />
{/* 语言切换和主题切换按钮 */}
<Space>
{/* <Button
size="small"
type="default"
onClick={handleLanguageChange}
>
{t(`language.${language === 'en' ? 'zh' : 'en'}`)}
</Button> */}
{/* 用户信息下拉菜单 */}
<Dropdown
menu={{
items: userMenuItems
}}
>
<div className="rb:cursor-pointer">{user.username}</div>
</Dropdown>
</Space>
{/* User info dropdown menu */}
<Dropdown
menu={{
items: userMenuItems
}}
>
<div className="rb:cursor-pointer">{user.username}</div>
</Dropdown>
<SettingModal
ref={settingModalRef}
/>

View File

@@ -1,6 +1,25 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:11:02
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:11:02
*/
/**
* AuthLayout Component
*
* The main authenticated layout wrapper that provides:
* - Route authentication and permission checks
* - Automatic breadcrumb navigation updates
* - Sidebar navigation and header
* - Token-based authentication validation
*
* @component
*/
import { Outlet } from 'react-router-dom';
import { useEffect, type FC } from 'react';
import { Layout } from 'antd';
import useRouteGuard from '@/hooks/useRouteGuard';
import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs';
import AppHeader from '@/components/Header';
@@ -11,13 +30,20 @@ import { cookieUtils } from '@/utils/request';
const { Content } = Layout;
// 认证布局组件使用useRouteGuard hook进行路由鉴权
/**
* Authentication layout component that wraps all authenticated pages.
* Handles route guards, breadcrumb navigation, and user authentication.
*/
const AuthLayout: FC = () => {
const { getUserInfo } = useUser();
// 使用路由守卫hook处理认证和权限检查
// Use route guard hook to handle authentication and permission checks
useRouteGuard('manage');
// 自动更新面包屑导航
// Automatically update breadcrumb navigation based on current route
useNavigationBreadcrumbs('manage');
// Check authentication token and fetch user info on mount
useEffect(() => {
const authToken = cookieUtils.get('authToken')
if (!authToken && !window.location.hash.includes('#/login')) {
@@ -29,9 +55,12 @@ const AuthLayout: FC = () => {
return (
<Layout style={{ minHeight: '100vh' }}>
{/* Sidebar navigation */}
<Sider />
<Layout style={{maxHeight: '100vh', width: '100vh', overflowY: 'auto' }}>
{/* Header with breadcrumbs and user menu */}
<AppHeader />
{/* Main content area - renders child routes */}
<Content style={{ padding: '16px 17px 24px 16px', zIndex: 0 }}>
<Outlet />
</Content>

View File

@@ -1,6 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:11:43
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:11:43
*/
/**
* AuthSpaceLayout Component
*
* The authenticated layout wrapper for knowledge base (space) pages that provides:
* - Route authentication and permission checks for space context
* - Automatic breadcrumb navigation updates
* - Sidebar navigation and header configured for space mode
* - Token-based authentication validation
* - Storage type initialization
*
* @component
*/
import { Outlet } from 'react-router-dom';
import { useEffect, type FC } from 'react';
import { Layout } from 'antd';
import useRouteGuard from '@/hooks/useRouteGuard';
import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs';
import AppHeader from '@/components/Header';
@@ -11,28 +31,38 @@ import { cookieUtils } from '@/utils/request';
const { Content } = Layout;
// 认证布局组件使用useRouteGuard hook进行路由鉴权
/**
* Authentication layout component for knowledge base (space) pages.
* Similar to AuthLayout but configured for space context with storage type management.
*/
const AuthSpaceLayout: FC = () => {
const { getUserInfo, getStorageType } = useUser();
// 使用路由守卫hook处理认证和权限检查
// Use route guard hook to handle authentication and permission checks for space context
useRouteGuard('space');
// 自动更新面包屑导航
// Automatically update breadcrumb navigation based on current route in space context
useNavigationBreadcrumbs('space');
// Check authentication token, fetch user info and storage type on mount
useEffect(() => {
const authToken = cookieUtils.get('authToken')
if (!authToken && !window.location.hash.includes('#/login')) {
window.location.href = `/#/login`;
} else {
getUserInfo()
getStorageType()
getStorageType() // Fetch storage type for knowledge base operations
}
}, []);
return (
<Layout style={{ minHeight: '100vh' }}>
{/* Sidebar navigation configured for space mode */}
<Sider source="space" />
<Layout style={{maxHeight: '100vh', width: '100vh', overflowY: 'auto' }}>
{/* Header with breadcrumbs and user menu configured for space mode */}
<AppHeader source="space" />
{/* Main content area for knowledge base pages - renders child routes */}
<Content style={{ padding: '16px 17px 24px 16px', zIndex: 0, height: 'calc(100vh - 64px)', overflowY: 'auto' }}>
<Outlet />
</Content>

View File

@@ -1,12 +1,35 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:12:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:12:42
*/
/**
* BasicLayout Component
*
* A minimal layout wrapper that provides:
* - User information initialization
* - Storage type initialization
* - Simple container for child routes without navigation UI
*
* Used for pages that don't require sidebar/header (e.g., login, public pages).
*
* @component
*/
import { Outlet } from 'react-router-dom';
import { useEffect, type FC } from 'react';
import { useUser } from '@/store/user';
// 基础布局组件,用于展示内容并保留用户信息获取功能
/**
* Basic layout component for pages without navigation UI.
* Fetches user info and storage type on mount, then renders child routes.
*/
const BasicLayout: FC = () => {
const { getUserInfo, getStorageType } = useUser();
// 获取用户信息
// Fetch user information and storage type on component mount
useEffect(() => {
getUserInfo();
getStorageType()
@@ -14,6 +37,7 @@ const BasicLayout: FC = () => {
return (
<div className="rb:relative rb:h-full rb:w-full">
{/* Render child routes without additional UI */}
<Outlet />
</div>
)

View File

@@ -1,13 +1,37 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:13:20
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:13:20
*/
/**
* LayoutBg Component
*
* A decorative background component that displays styled background elements.
* Provides visual aesthetics with positioned decorative shapes.
*
* @component
*/
import { type FC } from 'react';
import clsx from 'clsx';
import styles from './layout.module.css';
/**
* Background layout component with decorative elements.
* Renders a fixed full-screen background with styled shapes.
*/
const LayoutBg: FC = () => {
return (
<div className="rb:fixed rb:top-0 rb:right-0 rb:left-0 rb:bottom-0 rb:bg-[#FBFDFF]">
<div className={clsx('rb:h-[240px]', styles.bgTop)}>
{/* Top section with decorative background shapes */}
<div className={clsx('rb:h-60', styles.bgTop)}>
{/* Left decorative element 1 */}
<div className={clsx(styles.left1)}></div>
{/* Left decorative element 2 */}
<div className={clsx(styles.left2)}></div>
{/* Right decorative element */}
<div className={clsx(styles.right1)}></div>
</div>
</div>

View File

@@ -1,11 +1,30 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:13:38
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:13:38
*/
/**
* LoginLayout Component
*
* A minimal layout wrapper for authentication pages (login, register, etc.).
* Provides a simple container without navigation UI or authentication checks.
*
* @component
*/
import { Outlet } from 'react-router-dom';
import { type FC } from 'react';
// 基础布局组件,用于展示内容并保留用户信息获取功能
/**
* Login layout component for unauthenticated pages.
* Renders child routes in a simple full-size container.
*/
const LoginLayout: FC = () => {
return (
<div className="rb:relative rb:h-full rb:w-full">
{/* Render authentication pages (login, register, etc.) */}
<Outlet />
</div>
)

View File

@@ -1,11 +1,30 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:13:55
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:52:17
*/
/**
* NoAuthLayout Component
*
* A minimal layout wrapper for public pages that don't require authentication.
* Provides a simple container without navigation UI or authentication checks.
*
* @component
*/
import { Outlet } from 'react-router-dom';
import { type FC } from 'react';
// 基础布局组件,用于展示内容并保留用户信息获取功能
/**
* No-authentication layout component for public pages.
* Renders child routes in a simple full-size container without any auth requirements.
*/
const NoAuthLayout: FC = () => {
return (
<div className="rb:relative rb:h-full rb:w-full">
{/* Render public pages without authentication */}
<Outlet />
</div>
)

View File

@@ -1,15 +1,31 @@
import { memo } from 'react'
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:14:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:14:59
*/
/**
* AudioBlock Component
*
* Renders audio elements from markdown nodes.
* Extracts audio source URLs and creates HTML audio players with controls.
*
* @component
*/
import type { FC } from 'react'
import { memo, type FC } from 'react'
/** Props interface for AudioBlock component */
interface AudioBlockProps {
node: {
children: { properties: { src: string } }[]
}
}
/** Audio block component that renders audio elements from markdown nodes */
const AudioBlock: FC<AudioBlockProps> = (props) => {
// console.log('AudioBlock', props)
const { children } = props.node;
/** Extract audio source URLs from node children and filter out empty values */
const srcs = children.map(item => item.properties?.src).filter(item => item)
return (

View File

@@ -1,22 +1,45 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:15:05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:15:05
*/
/**
* Code Component
*
* A versatile code rendering component that supports:
* - Syntax-highlighted code blocks
* - ECharts visualizations
* - SVG rendering
* - Mermaid diagrams
* - Inline code snippets
*
* @component
*/
import { type FC, useMemo } from 'react'
import SyntaxHighlighter from 'react-syntax-highlighter';
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import CopyBtn from './CopyBtn';
import ReactEcharts from 'echarts-for-react';
import CopyBtn from './CopyBtn';
import Svg from './Svg'
import MermaidChart from './MermaidChart'
/** Props interface for Code component */
type ICodeProps = {
children: string;
className: string;
}
/** Code block component that renders syntax-highlighted code or special visualizations */
const Code: FC<ICodeProps> = (props) => {
const { children, className } = props;
/** Extract language from className (e.g., 'language-javascript' -> 'javascript') */
const language = className?.split('-')[1]
console.log('Code', props)
// Parse ECharts configuration from code content
const charData = useMemo(() => {
if (language !== 'echarts') return null;
try {
@@ -27,6 +50,7 @@ const Code: FC<ICodeProps> = (props) => {
}
}, [language, children])
// Render ECharts visualization
if (language === 'echarts') {
return (
<ReactEcharts
@@ -39,6 +63,7 @@ const Code: FC<ICodeProps> = (props) => {
)
}
// Render SVG content
if (language === 'svg') {
return (
<Svg
@@ -46,6 +71,7 @@ const Code: FC<ICodeProps> = (props) => {
/>
)
}
// Render Mermaid diagram
if (language === 'mermaid') {
return (
<MermaidChart
@@ -54,6 +80,7 @@ const Code: FC<ICodeProps> = (props) => {
)
}
// Render syntax-highlighted code block with copy button
if (className) {
return (
<div className="rb:relative">
@@ -81,6 +108,7 @@ const Code: FC<ICodeProps> = (props) => {
</div>
)
}
// Render inline code
return <code className="rb:bg-[#F0F3F8] rb:px-1 rb:py-0.5 rb:rounded rb:text-sm rb:font-mono rb:whitespace-break-spaces">{children}</code>
}

View File

@@ -1,9 +1,27 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:15:11
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:15:11
*/
/**
* CodeBlock Component
*
* A standalone code block component for displaying formatted code with:
* - Syntax highlighting
* - Optional copy functionality
* - Configurable size and line numbers
*
* @component
*/
import { type FC } from 'react'
import SyntaxHighlighter from 'react-syntax-highlighter';
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import CopyBtn from './CopyBtn';
/** Props interface for CodeBlock component */
type ICodeBlockProps = {
value: string;
needCopy?: boolean;
@@ -11,12 +29,7 @@ type ICodeBlockProps = {
showLineNumbers?: boolean;
}
// enum languageType {
// echarts = 'echarts',
// mermaid = 'mermaid',
// svg = 'svg',
// }
/** Code block component for displaying formatted code with optional copy functionality */
const CodeBlock: FC<ICodeBlockProps> = ({
value,
needCopy = true,

View File

@@ -1,15 +1,31 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:15:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:15:21
*/
/**
* CopyBtn Component
*
* A button component that copies text to clipboard and displays a success message.
* Uses the copy-to-clipboard library for cross-browser compatibility.
*
* @component
*/
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import copy from 'copy-to-clipboard'
import { Button, App } from 'antd'
/** Props interface for CopyBtn component */
type ICopyBtnProps = {
value: string;
className?: string;
style?: React.CSSProperties;
}
/** Copy button component that copies text to clipboard and shows success message */
const CopyBtn: FC<ICopyBtnProps> = ({
value,
className,
@@ -18,6 +34,7 @@ const CopyBtn: FC<ICopyBtnProps> = ({
const { t } = useTranslation()
const { message } = App.useApp()
/** Copy value to clipboard and show success message */
const handleCopy = () => {
copy(value)
message.success(t('common.copySuccess'))

View File

@@ -1,13 +1,29 @@
import { memo } from 'react'
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:15:55
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:15:55
*/
/**
* Link Component
*
* A secure link component that opens URLs in a new tab.
* Includes security attributes (noopener, noreferrer) to prevent security vulnerabilities.
*
* @component
*/
import { memo } from 'react'
import type { FC, ReactNode } from 'react'
/** Props interface for Link component */
interface LinkProps {
href: string;
children: ReactNode;
}
/** Link component that opens in a new tab with security attributes */
const Link: FC<LinkProps> = (props) => {
// console.log('Link', props)
const { children, href } = props;
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
}

View File

@@ -1,8 +1,26 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:16:01
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:16:01
*/
/**
* MermaidChart Component
*
* Renders Mermaid diagrams as images.
* - Converts Mermaid syntax to SVG
* - Converts SVG to base64 data URL for display
* - Generates unique IDs based on content hash
*
* @component
*/
import { useRef, useEffect, useState, type FC } from 'react'
import mermaid from 'mermaid'
import CryptoJS from 'crypto-js'
import { Image } from 'antd'
/** Initialize Mermaid with default configuration */
mermaid.initialize({
startOnLoad: true,
theme: 'default',
@@ -12,6 +30,7 @@ mermaid.initialize({
},
})
/** Convert SVG string to base64 data URL for image display */
const svgToBase64 = (svgGraph: string) => {
const svgBytes = new TextEncoder().encode(svgGraph)
const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
@@ -22,8 +41,11 @@ const svgToBase64 = (svgGraph: string) => {
reader.readAsDataURL(blob)
})
}
/** Mermaid chart component that renders Mermaid diagrams as images */
const MermaidChart: FC<{ content: string }> = ({ content }) => {
const [chartSvg, setChartSvg] = useState<string>('')
/** Generate unique ID based on content hash to avoid conflicts */
const id = useRef(`mermaidchart_${CryptoJS.MD5(content).toString()}`)
useEffect(() => {
@@ -33,6 +55,7 @@ const MermaidChart: FC<{ content: string }> = ({ content }) => {
drawDiagram()
}, [content])
/** Render Mermaid diagram and convert to base64 image */
const drawDiagram = async function () {
const { svg } = await mermaid.render(id.current, content);

View File

@@ -1,15 +1,30 @@
import { memo } from 'react'
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:16:06
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:16:06
*/
/**
* Paragraph Component
*
* A simple paragraph component for rendering markdown paragraphs.
*
* @component
*/
import { memo } from 'react'
import type { FC, ReactNode } from 'react'
/** Props interface for Paragraph component */
interface ParagraphProps {
node: {
children: ReactNode;
};
children: string[]
}
/** Paragraph component for rendering markdown paragraphs */
const Paragraph: FC<ParagraphProps> = (props) => {
// console.log('Paragraph', props)
const { children } = props
return <p>{children}</p>

View File

@@ -1,15 +1,32 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:16:10
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:16:10
*/
/**
* RbButton Component
*
* A button component for rendering buttons in markdown content.
* Wraps Ant Design Button component.
*
* @component
*/
import { memo } from 'react'
import type { FC, ReactNode } from 'react'
import { Button } from 'antd'
/** Props interface for RbButton component */
interface RbButtonProps {
node: {
children: ReactNode;
};
children: string[]
}
/** Button component for rendering buttons in markdown */
const RbButton: FC<RbButtonProps> = (props) => {
console.log('RbButton', props)
const { children } = props;
return (

View File

@@ -1,15 +1,28 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:16:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:16:14
*/
/**
* Svg Component
*
* Renders SVG content from string using dangerouslySetInnerHTML.
* Used for displaying SVG code blocks in markdown.
*
* @component
*/
import * as React from 'react';
/** Props interface for Svg component */
interface SvgProps {
content: string;
}
/**
* 渲染SVG内容的组件
*/
/** Component for rendering SVG content from string */
function Svg(props: SvgProps): JSX.Element {
const { content } = props;
// console.log('Svg', props)
return React.createElement(
'div',

View File

@@ -1,15 +1,32 @@
import { memo } from 'react'
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:16:18
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:54:55
*/
/**
* VideoBlock Component
*
* Renders video elements from markdown nodes.
* Extracts video source URLs and creates HTML video players with controls.
*
* @component
*/
import { memo } from 'react'
import type { FC } from 'react'
/** Props interface for VideoBlock component */
interface VideoBlockProps {
node: {
children: { properties: { src: string } }[]
}
}
/** Video block component that renders video elements from markdown nodes */
const VideoBlock: FC<VideoBlockProps> = (props) => {
// console.log('VideoBlock', props)
const { children } = props.node;
/** Extract video source URLs from node children and filter out empty values */
const srcs = children.map(item => item.properties?.src).filter(item => item)
return (

View File

@@ -1,3 +1,28 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:17:31
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:17:31
*/
/**
* RbMarkdown Component
*
* A comprehensive markdown renderer with support for:
* - Standard markdown syntax (headings, lists, tables, etc.)
* - Code syntax highlighting
* - Math equations (KaTeX)
* - Mermaid diagrams
* - ECharts visualizations
* - SVG rendering
* - Audio/video embedding
* - Interactive form elements
* - HTML comments visibility toggle
* - Editable mode with live preview
*
* @component
*/
import { useState, useRef, useEffect, type FC } from 'react'
import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd'
import ReactMarkdown from 'react-markdown'
import RemarkGfm from 'remark-gfm'
@@ -5,8 +30,6 @@ import RemarkMath from 'remark-math'
import RemarkBreaks from 'remark-breaks'
import RehypeKatex from 'rehype-katex'
import RehypeRaw from 'rehype-raw'
import type { FC } from 'react'
import { useState, useRef, useEffect } from 'react'
import Code from './Code'
import VideoBlock from './VideoBlock'
@@ -14,14 +37,21 @@ import AudioBlock from './AudioBlock'
import Link from './Link'
import RbButton from './RbButton'
/** Props interface for RbMarkdown component */
interface RbMarkdownProps {
/** Markdown content to render */
content: string;
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false隐藏
editable?: boolean; // 是否可编辑,默认为 false
onContentChange?: (content: string) => void; // 内容变化回调
/** Whether to display HTML comments (default: false) */
showHtmlComments?: boolean;
/** Whether the content is editable (default: false) */
editable?: boolean;
/** Callback fired when content changes in edit mode */
onContentChange?: (content: string) => void;
/** Additional CSS classes */
className?: string;
}
/** Custom component mappings for markdown elements */
const components = {
h1: ({ children, ...props }: any) => <h1 className="rb:text-2xl rb:font-bold rb:mb-2" {...props}>{children}</h1>,
h2: ({ children, ...props }: any) => <h2 className="rb:text-xl rb:font-bold rb:mb-2" {...props}>{children}</h2>,
@@ -38,7 +68,7 @@ const components = {
em: ({ children, ...props }: any) => <em className="rb:italic" {...props}>{children}</em>,
del: ({ children, ...props }: any) => <del className="rb:line-through" {...props}>{children}</del>,
span: ({ children, style, ...restProps }: any) => {
// 如果是 HTML 注释的 span应用特殊样式
// Apply special styling for HTML comment spans
if (style?.color === '#999') {
return <span style={{ color: '#999', fontSize: '0.9em' }}>{children}</span>
}
@@ -104,30 +134,33 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
const [editContent, setEditContent] = useState(content)
const textareaRef = useRef<any>(null)
// 当外部 content 变化时,同步更新编辑内容
/** Sync edit content when external content changes */
useEffect(() => {
setEditContent(content)
}, [content])
// 处理 textarea 内容变化
/** Handle textarea content changes and trigger callback */
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newContent = e.target.value
setEditContent(newContent)
// 实时回调内容变化
/** Trigger real-time content change callback */
onContentChange?.(newContent)
}
// 根据参数决定是否将 HTML 注释转换为可见文本
// 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤
/**
* Process content based on showHtmlComments flag
* Converts HTML comments to visible text when showHtmlComments is true
* Uses special span markup to display comments with styling
*/
const processedContent = showHtmlComments
? (editable ? editContent : content).replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
// 转换为带样式的文本,使用 <span class="html-comment"> 标记
/** Convert to styled text using span with html-comment class */
const escaped = commentContent.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;')
return `<span class="html-comment">&lt;!-- ${escaped} --&gt;</span>`
})
: (editable ? editContent : content)
// 如果是编辑模式,显示 textarea
/** Render textarea in edit mode */
if (editable) {
return (
<div className="rb:relative">
@@ -138,21 +171,21 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
}
`}</style>
{/* 编辑区域 */}
{/* Edit area with textarea */}
<Input.TextArea
ref={textareaRef}
value={editContent}
onChange={handleTextareaChange}
rows={10}
className="rb:font-mono rb:text-sm"
placeholder="请输入 Markdown 内容..."
placeholder="Enter Markdown content..."
style={{ resize: 'vertical' }}
/>
</div>
)
}
// 处理键盘快捷键
/** Handle keyboard shortcuts (e.g., Ctrl+C for copy) */
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
const selection = window.getSelection()
@@ -162,7 +195,7 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
}
}
// 预览模式
/** Render markdown preview mode */
return (
<div className={`rb:relative ${className || ''}`} onKeyDown={handleKeyDown} tabIndex={0}>
<style>{`

View File

@@ -1,12 +1,33 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:18:19
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 15:44:42
*/
/**
* PageScrollList Component
*
* An infinite scroll list component with pagination support that:
* - Automatically loads more data when scrolling to bottom
* - Supports grid layout with configurable columns
* - Handles loading and empty states
* - Exposes refresh method via ref
*
* @component
*/
import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
import { List } from 'antd';
import InfiniteScroll from 'react-infinite-scroll-component';
import { request } from '@/utils/request';
import PageEmpty from '@/components/Empty/PageEmpty'
import PageLoading from '@/components/Empty/PageLoading'
/** Default page size for pagination */
const PAGE_SIZE = 20;
/** API response structure with pagination metadata */
interface ApiResponse<T> {
items?: T[];
page: {
@@ -16,18 +37,28 @@ interface ApiResponse<T> {
hasnext: boolean;
};
}
/** Ref methods exposed to parent component */
export interface PageScrollListRef {
refresh: () => void;
}
/** Props interface for PageScrollList component */
interface PageScrollListProps<T, Q = Record<string, unknown>> {
/** API endpoint URL */
url: string;
/** Function to render each list item */
renderItem: (item: T) => React.ReactNode;
/** Query parameters for API request */
query?: Q;
/** Number of columns in grid layout */
column?: number;
/** Additional CSS classes */
className?: string;
needLoading?: boolean;
}
/** Infinite scroll list component with pagination support */
const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
renderItem,
query,
@@ -36,6 +67,7 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
className = '',
needLoading = true,
}: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => {
/** Expose refresh method to parent component */
useImperativeHandle(ref, () => ({
refresh,
}));
@@ -45,6 +77,7 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
const [hasMore, setHasMore] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
/** Load more data from API with pagination */
const loadMoreData = (flag?: boolean) => {
if (!flag && (loading || !hasMore)) {
return;
@@ -58,6 +91,7 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
.then((res) => {
const response = res as ApiResponse<T>;
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response as T[] : [];
// Replace data if flag is true, otherwise append
if (flag) {
setData(results);
} else {
@@ -78,17 +112,19 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
});
};
// 刷新列表数据
/** Reset list to initial state and reload data */
const refresh = () => {
setPage(1);
setHasMore(true);
setData([]);
}
/** Refresh when query parameters change */
useEffect(() => {
refresh()
}, [query]);
/** Load initial data when list is reset */
useEffect(() => {
if (page === 1 && hasMore && data.length === 0) {
loadMoreData(true);
@@ -111,6 +147,7 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
scrollableTarget="scrollableDiv"
className='rb:h-full!'
>
{/* Render grid list or empty state */}
{data.length > 0 ? (
<List
grid={{ gutter: 16, column: column }}

View File

@@ -1,7 +1,27 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:18:50
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:18:50
*/
/**
* PageTabs Component
*
* A styled wrapper around Ant Design's Segmented component for page-level tab navigation.
* Provides consistent styling for tab interfaces across the application.
*
* @component
*/
import { type FC } from 'react';
import { Segmented, type SegmentedProps } from 'antd';
import styles from './index.module.css';
/**
* Page tabs component wrapper for Ant Design Segmented component.
* Applies custom styling via CSS modules.
*/
const PageTabs: FC<SegmentedProps> = ({
value,
options,

View File

@@ -1,24 +1,59 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:19:30
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:19:30
*/
/**
* RadioGroupCard Component
*
* A radio group component that displays options as selectable cards with:
* - Visual card-based selection interface
* - Optional icons and descriptions
* - Support for clear selection
* - Block or inline layout modes
* - Custom item rendering
*
* @component
*/
import { type FC, type Key, type ReactNode, useEffect } from 'react';
import { type RadioGroupProps } from 'antd';
import clsx from 'clsx'
/** Radio card option interface */
interface RadioCardOption {
/** Option value */
value: string | number | boolean | null | undefined | Key;
/** Option label text */
label: string;
/** Optional description text */
labelDesc?: string;
/** Optional icon URL */
icon?: string;
/** Whether the option is disabled */
disabled?: boolean;
/** Additional properties */
[key: string]: string | number | boolean | undefined | null | Key;
}
/** Props interface for RadioGroupCard component */
interface RadioCardProps extends Omit<RadioGroupProps, 'onChange'> {
/** Array of radio card options */
options: RadioCardOption[];
/** Callback fired when value changes (for side effects) */
onValueChange?: (value: string | null | undefined, option?: RadioCardOption) => void;
/** Callback fired when selection changes */
onChange?: (value: string | null | undefined, option?: RadioCardOption) => void;
/** Custom render function for each option */
itemRender?: (option: RadioCardOption) => ReactNode;
/** Whether clicking selected option clears selection */
allowClear?: boolean;
/** Whether to display cards in block (vertical) layout */
block?: boolean;
}
/** Radio group card component that displays options as selectable cards */
const RadioGroupCard: FC<RadioCardProps> = ({
options,
value,
@@ -28,16 +63,19 @@ const RadioGroupCard: FC<RadioCardProps> = ({
allowClear = true,
block = false,
}) => {
// 监听value变化
/** Listen to value changes and trigger side effects via onValueChange callback */
useEffect(() => {
if (onValueChange) {
onValueChange(value);
}
}, [value, onValueChange]);
/** Handle option selection with support for clear and disabled states */
const handleChange = (option: RadioCardOption) => {
// Ignore clicks on disabled options
if (option.disabled) return
if (onChange) {
// Clear selection if allowClear is true and option is already selected
if (allowClear && value === option.value) {
onChange(null, undefined);
} else {
@@ -51,6 +89,7 @@ const RadioGroupCard: FC<RadioCardProps> = ({
'rb:gap-3': !block,
'rb:gap-4': block,
})}>
{/* Render each option as a selectable card */}
{options.map(option => (
<div key={String(option.value)} className={clsx("rb:border rb:rounded-lg rb:w-full rb:p-[20px_12px] rb:text-center rb:cursor-pointer", {
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF]': option.value === value,
@@ -58,6 +97,7 @@ const RadioGroupCard: FC<RadioCardProps> = ({
'rb:opacity-[0.75]': option.disabled,
'rb:flex rb:items-center rb:text-left rb:gap-4': block,
})} onClick={() => handleChange(option)}>
{/* Use custom render or default card layout */}
{itemRender ? itemRender(option) : (
<>
{option.icon && <img src={option.icon} className={clsx("rb:w-10 rb:h-10", {

View File

@@ -1,12 +1,33 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:19:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:19:59
*/
/**
* RbAlert Component
*
* A custom alert component with predefined color themes and optional icon support.
* Provides consistent styling for informational messages across the application.
*
* @component
*/
import { type FC, type ReactNode } from 'react'
/** Props interface for RbAlert component */
interface RbAlertProps {
/** Color theme for the alert */
color?: 'blue' | 'green' | 'orange' | 'purple',
/** Alert content */
children: ReactNode | string;
/** Optional icon to display before content */
icon?: ReactNode;
/** Additional CSS classes */
className?: string;
}
/** Color theme mappings with text, background, and border colors */
const colors = {
blue: 'rb:text-[rgba(21,94,239,1)] rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]',
green: 'rb:text-[rgba(54,159,33,1)] rb:bg-[rgba(54,159,33,0.08)] rb:border-[rgba(54,159,33,0.30)]',
@@ -14,6 +35,7 @@ const colors = {
purple: 'rb:text-[rgba(156,111,255,1)] rb:bg-[rgba(156,111,255,0.08)] rb:border-[rgba(156,111,255,0.30)]',
}
/** Custom alert component with color themes and optional icon */
const RbAlert: FC<RbAlertProps> = ({ color = 'blue', icon, className, children }) => {
return (
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-4 rb:border rb:rounded-md`}>

View File

@@ -1,24 +1,59 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-02 15:21:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-02 15:21:14
*/
/**
* RbCard Component
*
* A customizable card component that extends Ant Design's Card with:
* - Multiple header styles (border, borderless, borderBL, borderL)
* - Avatar support with image or custom component
* - Flexible padding and styling options
* - Tooltip support for long titles
* - Hover effects
*
* @component
*/
import { type FC, type ReactNode } from 'react'
import { Card, Tooltip } from 'antd';
import clsx from 'clsx';
/** Props interface for RbCard component */
interface RbCardProps {
/** Additional CSS classes for header */
headerClassName?: string;
/** Card title (string, ReactNode, or function) */
title?: string | ReactNode | (() => ReactNode);
/** Subtitle text displayed below title */
subTitle?: string | ReactNode;
/** Extra content displayed in header (top-right) */
extra?: ReactNode;
/** Card body content */
children?: ReactNode;
/** Custom avatar component */
avatar?: ReactNode;
/** Avatar image URL */
avatarUrl?: string | null;
/** Custom padding for card body */
bodyPadding?: string;
/** Additional CSS classes for body */
bodyClassName?: string;
/** Header style variant */
headerType?: 'border' | 'borderless' | 'borderBL' | 'borderL';
/** Background color */
bgColor?: string;
/** Card height */
height?: string;
/** Additional CSS classes */
className?: string;
/** Click handler */
onClick?: () => void;
}
/** Custom card component with flexible styling and header options */
const RbCard: FC<RbCardProps> = ({
headerClassName,
title,
@@ -35,6 +70,7 @@ const RbCard: FC<RbCardProps> = ({
className,
...props
}) => {
/** Calculate body padding based on header type and avatar presence */
const bodyClassName = bodyPadding
? `rb:p-[${bodyPadding}]!`
: headerType === 'borderL'
@@ -46,11 +82,13 @@ const RbCard: FC<RbCardProps> = ({
: (headerType === 'border' && !avatarUrl && !avatar) || headerType === 'borderBL'
? 'rb:p-[16px_16px_20px_16px]!'
: ''
return (
<Card
{...props}
title={typeof title === 'function' ? title() : title ?
<div className="rb:flex rb:items-center rb:gap-2">
{/* Avatar image or custom avatar component */}
{avatarUrl
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
: avatar ? avatar : null
@@ -63,7 +101,9 @@ const RbCard: FC<RbCardProps> = ({
}
)
}>
{/* Title with tooltip for overflow text */}
<Tooltip title={title}><div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div></Tooltip>
{/* Optional subtitle */}
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
</div>
</div> : null
@@ -73,10 +113,15 @@ const RbCard: FC<RbCardProps> = ({
header: clsx(
'rb:font-medium',
{
/** Borderless header style */
'rb:border-[0]! rb:text-[16px] rb:p-[0_16px]!': headerType === 'borderless',
/** Header with avatar */
'rb:border-[0]! rb:text-[16px] rb:p-[16px_16px_0_16px]!': avatarUrl || avatar,
/** Standard border header */
'rb:text-[18px] rb:p-[0]! rb:m-[0_20px]!': headerType === 'border' && !avatarUrl && !avatar,
/** Border bottom-left style */
"rb:m-[0_16px]! rb:p-[0]! rb:relative rb:before:content-[''] rb:before:w-[4px] rb:before:h-[16px] rb:before:bg-[#5B6167] rb:before:absolute rb:before:top-[50%] rb:before:left-[-16px] rb:before:translate-y-[-50%] rb:before:bg-[#5B6167]! rb:before:h-[16px]!": headerType === 'borderBL',
/** Border left style */
"rb:m-[0_16px]! rb:p-[0]! rb:leading-[20px] rb:min-h-[48px]! rb:relative rb:border-[0]! rb:before:content-[''] rb:before:w-[4px] rb:before:h-[16px] rb:before:bg-[#5B6167] rb:before:absolute rb:before:top-[50%] rb:before:left-[-16px] rb:before:translate-y-[-50%] rb:before:bg-[#5B6167]! rb:before:h-[16px]!": headerType === 'borderL',
},
headerClassName,

View File

@@ -1,73 +0,0 @@
import { type FC, type ReactNode } from 'react'
import { Card } from 'antd';
import clsx from 'clsx';
interface RbCardProps {
title?: string | ReactNode;
subTitle?: string;
extra?: ReactNode;
children: ReactNode;
avatar?: ReactNode;
className?: string;
}
const RbCard: FC<RbCardProps> = ({
title,
subTitle,
extra,
children,
avatar,
className,
}) => {
if (avatar) {
return (
<Card
classNames={{
header: 'rb:p-[0]! rb:m-[0_20px]!',
body: 'rb:p-[16px_20px_16px_16px]',
}}
style={{
background: '#FBFDFF'
}}
>
{title &&
<div className={clsx("rb:text-[#212332] rb:text-[16px] rb:font-medium rb:flex rb:items-center rb:mb-[20px]", {
'rb:justify-between': extra
})}>
<div className="rb:flex rb:items-center">
<div className="rb:mr-[13px] rb:w-[48px] rb:h-[48px] rb:rounded-[8px] rb:overflow-hidden">{avatar}</div>
<div className="rb:truncate">{title}</div>
</div>
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
{extra}
</div>
}
{children}
</Card>
)
}
return (
<Card
title={ title ?
<div className={clsx("rb:text-[#212332] rb:text-[18px] rb:font-medium rb:flex rb:items-center", {
'rb:justify-between': extra
})}>
<div className="rb:truncate">{title}</div>
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
{extra}
</div> : null
}
classNames={{
header: 'rb:p-[0]! rb:m-[0_20px]!',
body: `rb:p-[16px_20px_20px_16px] ${className || ''}`,
}}
style={{
background: '#FBFDFF'
}}
>
{children}
</Card>
)
}
export default RbCard

View File

@@ -3,14 +3,27 @@
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2025-11-07 14:16:33
* @LastEditors: yujiangping
* @LastEditTime: 2025-11-27 20:02:46
* @LastEditors: ZhaoYing
* @LastEditTime: 2026-02-02 15:23:01
*/
/**
* RbDrawer Component
*
* A customized drawer component that extends Ant Design's Drawer with:
* - Internal state management for open/close
* - Custom close button in header
* - Full-height flex layout for content
* - Automatic state synchronization with external control
*
* @component
*/
import { type FC, useState, useEffect } from 'react'
import { Button, Drawer, Space } from 'antd';
import type { DrawerProps } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
/** Custom drawer component with internal state management and custom close button */
const RbDrawer: FC<DrawerProps> =({
children,
size = 'large',
@@ -18,30 +31,32 @@ const RbDrawer: FC<DrawerProps> =({
onClose,
...props
}) => {
// 内部状态管理,组件内部完全控制 open 状态
/** Internal state management - component fully controls open state internally */
const [internalOpen, setInternalOpen] = useState(false);
// 当外部 open 变化时,同步到内部状态
/** Sync internal state when external open prop changes */
useEffect(() => {
if (externalOpen !== undefined) {
setInternalOpen(externalOpen);
}
}, [externalOpen]);
// 确保当外部 open true 时,内部状态也同步为 true处理重复打开的情况
/** Ensure internal state syncs to true when external open is true (handles repeated opening) */
useEffect(() => {
if (externalOpen === true && !internalOpen) {
setInternalOpen(true);
}
}, [externalOpen, internalOpen]);
/** Handle drawer close - updates internal state and notifies parent */
const handleClose = (e: React.MouseEvent | React.KeyboardEvent) => {
// 更新内部状态,关闭抽屉
/** Update internal state to close drawer */
setInternalOpen(false);
// 如果外部传入了 onClose调用它通知外部
/** If external onClose is provided, call it to notify parent */
onClose?.(e);
}
/** Handle close button click */
const handleButtonClose = (e: React.MouseEvent) => {
handleClose(e);
}
@@ -56,11 +71,13 @@ const RbDrawer: FC<DrawerProps> =({
open={internalOpen}
extra={
<Space>
{/* Custom close button in header */}
<Button type='text' icon={<CloseOutlined />} onClick={handleButtonClose}/>
</Space>
}
{...props}
>
{/* Full-height flex container for content */}
<div className='rb:flex rb:flex-col rb:h-full'>
{children}
</div>

Some files were not shown because too many files have changed in this diff Show More