Merge branch 'develop' into docs/web_zy

This commit is contained in:
yingzhao
2026-02-04 10:29:03 +08:00
committed by GitHub
105 changed files with 5387 additions and 3130 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

@@ -3,6 +3,7 @@ from typing import Optional
from uuid import UUID
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
@@ -31,7 +32,7 @@ from app.services.memory_storage_service import (
search_entity,
search_statement,
)
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Header
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
@@ -280,17 +281,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

@@ -32,7 +32,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
}
# 获取节点更新信息
@@ -890,9 +891,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]]:
"""
获取指定用户的热门记忆标签
@@ -906,17 +905,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

@@ -57,6 +57,10 @@ const ALL_FILE_TYPE: {
htm: 'text/html',
html: 'text/html',
json: 'application/json',
owl: 'application/rdf+xml',
ttl: 'text/turtle',
rdf: 'application/rdf+xml',
xml: 'application/rdf+xml',
}
export interface UploadFilesRef {
fileList: UploadFile[];
@@ -122,7 +126,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
if (fileSize) {
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
if (!isLtMaxSize) {
message.error(`文件大小不能超过 ${fileSize}MB`);
message.error(t('common.fileSizeTip', { size: fileSize }));
return Upload.LIST_IGNORE;
}
}
@@ -139,7 +143,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
const isValidMimeType = file.type && accept ? accept.includes(file.type) : true;
if (!isValidExtension && !isValidMimeType) {
message.error(`不支持的文件类型: ${fileExtension || file.type}`);
message.error(`${t('common.fileAcceptTip')}${fileExtension || file.type}`);
return Upload.LIST_IGNORE;
}
}
@@ -236,12 +240,12 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
fileList,
beforeUpload,
headers: {
authorization: cookieUtils.get('authToken') || '',
authorization: `Bearer ${cookieUtils.get('authToken')}`,
},
onRemove: handleRemove,
onChange: handleChange,
accept,
disabled,
disabled: disabled || fileList.length >= maxCount,
showUploadList: {
showPreviewIcon: false,
showRemoveIcon: true,
@@ -249,12 +253,12 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
},
itemRender: (_, file, __, actions) => {
return (
<div key={file.uid} className="rb:relative rb:w-full rb:pt-[8px] rb:pl-[10px] rb:pr-[10px] rb-pb-[10px] rb:border-1 rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-[2px]">
<div key={file.uid} className="rb:relative rb:w-full rb:pt-2 rb:pl-2.5 rb:pr-2.5 rb-pb-[10px] rb:border rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-0.5">
{file.name}
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>Cancel</span>
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>{t('common.cancel')}</span>
</div>
<Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />
{isAutoUpload && <Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />}
</div>
);
},
@@ -267,20 +271,20 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
clearFiles
}));
const hasProgress = fileList.some((item) => item.percent !== 100);
const hasProgress = isAutoUpload && fileList.some((item) => item.percent !== 100);
if (isCanDrag) {
return (
<div className="rb:mb-[24px] rb:w-full">
<div className="rb:mb-6 rb:w-full">
<Dragger {...uploadProps} style={{ height: '270px' }}>
<div className="rb:flex rb:justify-center rb:flex-col rb:items-center">
<img className="rb:w-[48px] rb:h-[48px]" src={CloudUploadOutlined} />
{!hasProgress && (!fileList || !fileList.length) &&
<img className="rb:w-12 rb:h-12" src={CloudUploadOutlined} />
{(!isAutoUpload || !hasProgress && (!fileList || !fileList.length)) &&
<>
<div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-[8px] rb:leading-[20px]">
{t('common.dragUploadTip')}<span className="rb:ml-[4px] rb:text-[#155EEF]">{t('common.uploadClickTip')}</span>
<div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-2 rb:leading-5">
{t('common.dragUploadTip')}<span className="rb:ml-1 rb:text-[#155EEF]">{t('common.uploadClickTip')}</span>
</div>
{fileType && <div className="rb:text-[12px] rb:text-[#A8A9AA] rb:leading-[14px] rb:mt-[8px] rb:cursor-pointer">{t('common.supportedFileTypes', { types: fileType.join(',') })}</div>}
{fileType && <div className="rb:text-[12px] rb:text-[#A8A9AA] rb:leading-3.5 rb:mt-2 rb:cursor-pointer">{t('common.supportedFileTypes', { types: fileType.join(',') })}</div>}
{(fileSize || fileType || maxCount > 1) && (
<div className='rb:text-xs rb:mt-2 rb:text-[#A8A9AA]'>
{t('common.uploadFileTipMax', { max: fileSize, maxCount: maxCount })}
@@ -288,7 +292,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
)}
</>
}
{hasProgress && <div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-[8px] rb:mb-[24px] rb:leading-[20px]">{t('common.uploading')}</div>}
{hasProgress && <div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-2 rb:mb-6 rb:leading-5">{t('common.uploading')}</div>}
</div>
</Dragger>
</div>

View File

@@ -426,6 +426,7 @@ export const en = {
fileAcceptTip: 'Unsupported file type:',
nextStep: 'Next Step',
prevStep: 'Previous Step',
exportSuccess: 'Export successful',
},
model: {
searchPlaceholder: 'search model…',
@@ -2472,6 +2473,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
extract: 'Project Inference',
source: 'Not Added',
target: 'Added',
import: 'Import Scenario',
format: 'Export Format',
export: 'Export Scenario',
scene_id: 'Scenario',
file: 'Import File',
},
prompt: {
editor: 'Prompt Generator',

View File

@@ -980,6 +980,7 @@ export const zh = {
fileAcceptTip: '不支持的文件类型:',
nextStep: '下一步',
prevStep: '上一步',
exportSuccess: '导出成功',
},
product: {
applicationManagement: '应用管理',
@@ -2561,6 +2562,11 @@ export const zh = {
extract: '工程推理',
source: '未添加项',
target: '已添加项',
import: '导入场景',
format: '导出格式',
export: '导出场景',
scene_id: '场景',
file: '导入文件',
},
prompt: {
editor: '提示词生成器',

View File

@@ -330,19 +330,20 @@ export const request = {
...config
});
},
downloadFile(url: string, fileName: string, data?: unknown) {
downloadFile(url: string, fileName: string, data?: unknown, callback?: () => void) {
service.post(url, data, {
responseType: "blob",
})
.then(res =>{
const link = document.createElement("a");
const blob = new Blob([res.data], { type: "application/vnd.ms-excel" });
const blob = new Blob([res as unknown as BlobPart]);
link.style.display = "none";
link.href = URL.createObjectURL(blob);
link.setAttribute("download", decodeURI(res.headers['filename'] || fileName));
link.setAttribute("download", decodeURI(fileName || fileName));
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
callback?.()
});
}
};

View File

@@ -24,7 +24,7 @@ const { TextArea } = Input;
const radioWrapperBaseStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'flex-start',
columnGap: 14, // 点与文字更宽的间距
columnGap: 14, // Wider gap between dot and text
width: '100%',
border: '1px solid #E5E5E5',
borderRadius: 8,
@@ -97,7 +97,7 @@ const CreateDataset = () => {
() => [
{ title: t('knowledgeBase.selectFile') },
{ title: t('knowledgeBase.parameterSettings') },
// { title: t('knowledgeBase.dataPreview') }, // 暂时隐藏第三步
// { title: t('knowledgeBase.dataPreview') }, // Temporarily hide step 3
{ title: t('knowledgeBase.confirmUpload') },
],
[t],
@@ -105,27 +105,27 @@ const CreateDataset = () => {
// 存储每个文件的 AbortController用于取消上传
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
console.log('上传文件',uploadRef.current?.fileList.length)
console.log('Upload files', uploadRef.current?.fileList.length)
const handleNext = async () => {
// 暂时隐藏第三步调整步骤索引0->1->2 对应 选择文件->参数设置->确认上传)
// Temporarily hide step 3: adjust step index (0->1->2 corresponds to select file->parameter settings->confirm upload)
let nextStep = current + 1;
if(nextStep === 1 && source === 'local') {
// 检查是否有文件已上传
// Check if files have been uploaded
if (rechunkFileIds.length === 0) {
// 如果没有文件,提示用户先上传文件
// If no files, prompt user to upload first
Modal.warning({
title: t('common.warning') || '提示',
content: t('knowledgeBase.pleaseUploadFileFirst') || '请先上传文件',
title: t('common.warning') || 'Warning',
content: t('knowledgeBase.pleaseUploadFileFirst') || 'Please upload files first',
});
return; // 不进入下一步
return; // Don't proceed to next step
}
}else if(nextStep === 1 && source === 'text'){
try {
const values = await form.validateFields();
// setLoading(true);
// TODO: 这里需要调用相应的API来保存内容
// TODO: Need to call corresponding API to save content here
const params = {
// ...values,
kb_id: knowledgeBaseId,
@@ -162,41 +162,41 @@ const CreateDataset = () => {
})
}
// 立即执行一次,加载文档列表用于预览(不自动返回)
// Execute once immediately to load document list for preview (don't auto-return)
pollDocumentStatus(false);
}
// 限制最大步骤为 2确认上传
// Limit max step to 2 (confirm upload)
setCurrent(Math.min(nextStep, 2));
};
const handlePrev = () => setCurrent((c) => Math.max(c - 1, 0));
// 开始上传:触发文档解析并启动轮询
// Start upload: trigger document parsing and start polling
const handleStartUpload = () => {
if (rechunkFileIds.length === 0) {
Modal.warning({
title: t('common.warning') || '提示',
content: t('knowledgeBase.pleaseUploadFileFirst') || '请先上传文件',
title: t('common.warning') || 'Warning',
content: t('knowledgeBase.pleaseUploadFileFirst') || 'Please upload files first',
});
return;
}
// 显示确认弹框
confirm({
title: t('knowledgeBase.startUploadConfirmTitle') || '开始处理文档',
content: t('knowledgeBase.startUploadConfirmContent') || '文档处理将在后台进行,您可以选择立即返回列表页或停留在此页面查看处理进度。',
okText: t('knowledgeBase.returnToList') || '返回列表页',
cancelText: t('knowledgeBase.stayOnPage') || '停留在此页',
title: t('knowledgeBase.startUploadConfirmTitle') || 'Start processing documents',
content: t('knowledgeBase.startUploadConfirmContent') || 'Document processing will proceed in the background. You can choose to return to the list page immediately or stay on this page to view processing progress.',
okText: t('knowledgeBase.returnToList') || 'Return to list',
cancelText: t('knowledgeBase.stayOnPage') || 'Stay on this page',
onOk: () => {
// 用户选择返回列表页 - 不显示 loading直接跳转
// User chose to return to list - don't show loading, navigate directly
startProcessing(true);
},
onCancel: () => {
// 用户选择停留在当前页 - 显示 loading 并开始轮询
console.log('用户选择停留,开始显示 loading');
// User chose to stay on current page - show loading and start polling
console.log('User chose to stay, starting to show loading');
setPollingLoading(true);
// 延迟一点时间让用户看到 loading 效果,然后开始处理
// Delay a bit to let user see loading effect, then start processing
setTimeout(() => {
startProcessing(false);
}, 100);
@@ -204,25 +204,25 @@ const CreateDataset = () => {
});
};
// 实际开始处理的函数
// Function to actually start processing
const startProcessing = (autoReturnToList: boolean) => {
// 触发文档解析
// Trigger document parsing
rechunkFileIds.map((id) => {
parseDocument(id, {});
});
if (autoReturnToList) {
// 用户选择立即返回,直接跳转(不显示 loading
console.log('用户选择立即返回列表页');
// User chose to return immediately, navigate directly (no loading shown)
console.log('User chose to return to list page immediately');
handleBack();
} else {
// 用户选择停留启动轮询查看进度loading 已在 onCancel 中设置)
console.log('用户选择停留查看进度');
// User chose to stay, start polling to view progress (loading already set in onCancel)
console.log('User chose to stay and view progress');
// 立即执行一次轮询(启用自动返回)
// Execute polling once immediately (enable auto-return)
pollDocumentStatus(true);
// 然后每3秒执行一次启用自动返回
// Then execute every 3 seconds (enable auto-return)
pollingTimerRef.current = setInterval(() => {
pollDocumentStatus(true);
}, 3000);
@@ -244,11 +244,11 @@ const CreateDataset = () => {
},
onCancel: () => {
console.log('取消删除');
console.log('Delete cancelled');
},
});
}
// 表格列配置
// Table column configuration
const columns: ColumnsType = [
{
title: t('knowledgeBase.name'),
@@ -261,7 +261,7 @@ const CreateDataset = () => {
dataIndex: 'progress',
key: 'progress',
render: (value: number, record: any) => {
// value >= 1 时完成01 时显示进度条
// When value >= 1 it's complete, when 01 show progress bar
if (value >= 1) {
return (
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
@@ -270,7 +270,7 @@ const CreateDataset = () => {
</span>
);
} else if (value >= 0 && value < 1) {
// 处理中,显示进度条
// Processing, show progress bar
return (
<div className="rb:flex rb:items-center rb:gap-2">
<Progress
@@ -286,7 +286,7 @@ const CreateDataset = () => {
</div>
);
} else {
// value = 0 或其他情况,显示待处理
// value = 0 or other cases, show pending
return (
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: '#FF8A4C' }}></span>
@@ -304,7 +304,7 @@ const CreateDataset = () => {
),
},
];
// 检查媒体文件时长的辅助函数
// Helper function to check media file duration
const checkMediaDuration = (file: File): Promise<number> => {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
@@ -324,36 +324,36 @@ const CreateDataset = () => {
});
};
// 上传文件
// Upload file
const handleUpload = async (options: UploadRequestOption) => {
const { file, onSuccess, onError, onProgress, filename = 'file' } = options;
// 创建 AbortController 用于取消上传
// Create AbortController for cancelling upload
const abortController = new AbortController();
const fileUid = (file as any).uid;
abortControllersRef.current.set(fileUid, abortController);
// 获取文件扩展名
// Get file extension
const fileExtension = (file as File).name.split('.').pop()?.toLowerCase();
const mediaExtensions = ['mp3', 'mp4', 'mov', 'wav'];
// 如果是媒体文件,进行大小和时长检查
// If media file, check size and duration
if (fileExtension && mediaExtensions.includes(fileExtension)) {
const fileSizeInMB = (file as File).size / (1024 * 1024);
// 检查文件大小50MB限制
if (fileSizeInMB > 100) {
messageApi.error(`${t('knowledgeBase.sizeLimitError')}${fileSizeInMB.toFixed(2)}MB`);
messageApi.error(`${t('knowledgeBase.sizeLimitError')}: ${fileSizeInMB.toFixed(2)}MB`);
onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`));
abortControllersRef.current.delete(fileUid);
return;
}
try {
// 检查媒体时长150秒限制
// Check media duration (150 second limit)
const duration = await checkMediaDuration(file as File);
if (duration > 150) {
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}${Math.round(duration)}`);
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}: ${Math.round(duration)}s`);
onError?.(new Error(`${t('knowledgeBase.fileDurationExceeds')}`));
abortControllersRef.current.delete(fileUid);
return;
@@ -386,7 +386,7 @@ const CreateDataset = () => {
},
})
.then((res: UploadFileResponse) => {
// 上传成功,移除 AbortController
// Upload successful, remove AbortController
abortControllersRef.current.delete(fileUid);
onSuccess?.(res, new XMLHttpRequest());
@@ -399,12 +399,12 @@ const CreateDataset = () => {
}
})
.catch((error) => {
// 移除 AbortController
// Remove AbortController
abortControllersRef.current.delete(fileUid);
// 如果是用户主动取消,不显示错误信息
// If user actively cancelled, don't show error message
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
console.log('上传已取消:', (file as File).name);
console.log('Upload cancelled:', (file as File).name);
return;
}
onError?.(error as Error);
@@ -413,12 +413,12 @@ const CreateDataset = () => {
// 轮询检查文档处理状态
// autoReturn: 是否在所有文档完成时自动返回列表页
// autoReturn: whether to automatically return to list page when all documents are completed
const pollDocumentStatus = (autoReturn: boolean = false) => {
console.log('开始轮询文档状态,当前 pollingLoading:', pollingLoading);
console.log('Start polling document status, current pollingLoading:', pollingLoading);
if (!knowledgeBaseId || !parentId || rechunkFileIds.length === 0) {
console.log('轮询条件不满足,退出');
console.log('Polling conditions not met, exiting');
return;
}
@@ -436,10 +436,10 @@ const CreateDataset = () => {
}
console.log('documents', documents);
// 检查是否所有文档的 progress 都为 1
// Check if all documents have progress of 1
const allCompleted = documents.every((doc: KnowledgeBaseDocumentData) => doc.progress === 1);
console.log('轮询状态:', allCompleted);
console.log('Polling status:', allCompleted);
// 检查是否所有文档都完成了
// debugger
@@ -455,23 +455,23 @@ const CreateDataset = () => {
setPollingLoading(false);
}, 1000);
// 只有在 autoReturn true 时才自动返回
// Only auto-return when autoReturn is true
if (autoReturn) {
// 延迟 2 秒后跳转,让用户看到完成状态
console.log('所有文档处理完成2秒后返回列表页');
// Delay 2 seconds before navigating to let user see completion status
console.log('All documents processed, returning to list page in 2 seconds');
setTimeout(() => {
handleBack();
}, 2000);
} else {
console.log('所有文档处理完成,用户可手动操作');
console.log('All documents processed, user can operate manually');
}
} else {
// 如果还有文档在处理中,确保 loading 状态保持
console.log('还有文档在处理中,保持 loading 状态');
// If documents are still processing, keep loading state
console.log('Documents still processing, maintaining loading state');
}
})
.catch((error) => {
console.error('轮询文档状态失败:', error);
console.error('Failed to poll document status:', error);
setPollingLoading(false);
});
};
@@ -486,7 +486,7 @@ const CreateDataset = () => {
},
});
} else {
console.warn('缺少路由参数,无法返回');
console.warn('Missing route parameters, unable to return');
}
};
const handleChange = (value: number | null) =>{
@@ -498,17 +498,17 @@ const CreateDataset = () => {
const handleDeleteFile = async (fileId: string) => {
try {
await deleteDocument(fileId);
// 删除成功,从 rechunkFileIds 中移除该 id
// Delete successful, remove the id from rechunkFileIds
setRechunkFileIds((prev) => prev.filter((id) => id !== fileId));
console.log(`${t('common.deleteSuccess')}`);
} catch (error) {
messageApi.error(`${t('common.deleteFailed')}`);
}
};
// 当从其他页面跳转过来且带有 fileIds 时,加载对应的文档数据
// When navigating from other pages with fileIds, load corresponding document data
// useEffect(() => {
// if (initialFileIds.length > 0 && initialStepKey !== 'selectFile' && knowledgeBaseId && parentId) {
// // 加载文档列表数据
// // Load document list data
// getDocumentList(knowledgeBaseId,{
// document_ids: initialFileIds.join(','),
// })
@@ -517,12 +517,12 @@ const CreateDataset = () => {
// setData(documents);
// })
// .catch((error) => {
// console.error('加载文档列表失败:', error);
// console.error('Failed to load document list:', error);
// });
// }
// }, []);
// 清理函数:组件卸载时清除定时器和 loading 状态
// Cleanup function: clear timer and loading state when component unmounts
useEffect(() => {
return () => {
if (pollingTimerRef.current) {
@@ -533,10 +533,10 @@ const CreateDataset = () => {
};
}, []);
// 监听路由变化,确保在页面切换时清理状态
// Watch for route changes, ensure state is cleaned up when page switches
useEffect(() => {
return () => {
// 页面卸载时清理状态
// Clean up state when page unmounts
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
@@ -574,7 +574,7 @@ const CreateDataset = () => {
fileType={fileType}
customRequest={handleUpload}
onChange={(fileList) => {
console.log('文件列表变化:', fileList);
console.log('File list changed:', fileList);
}}
onRemove={async (file) => {
// 如果文件正在上传,取消上传
@@ -583,26 +583,26 @@ const CreateDataset = () => {
if (abortController) {
abortController.abort();
abortControllersRef.current.delete(fileUid);
console.log('已取消上传:', (file as any).name);
console.log('Upload cancelled:', (file as any).name);
// 取消上传后直接返回 true允许移除文件
return true;
}
// 只有当文件已经上传成功有response.id才删除服务器上的文件
// Only delete server file when file upload was successful (has response.id)
if (file.response?.id) {
try {
await deleteDocument(file.response.id);
setRechunkFileIds(prev => prev.filter(id => id !== file.response.id));
console.log('已删除服务器文件:', file.response.id);
console.log('Server file deleted:', file.response.id);
return true;
} catch (error) {
console.error('删除文件失败:', error);
messageApi.error(t('common.deleteFailed') || '删除文件失败');
return false; // 删除失败时不移除文件
console.error('Failed to delete file:', error);
messageApi.error(t('common.deleteFailed') || 'Failed to delete file');
return false; // Don't remove file when deletion fails
}
}
// 其他情况(如上传失败的文件)也允许移除
// Also allow removal in other cases (such as failed uploads)
return true;
}} />
)}

View File

@@ -1,5 +1,5 @@
/*
* @Description: 文档详情
/**
* @Description: Document Details
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2025-11-15 16:13:47
@@ -57,28 +57,28 @@ const DocumentDetails: FC = () => {
}
}, [documentId]);
// 更新面包屑
// Update breadcrumbs
useEffect(() => {
if (breadcrumbPath) {
updateBreadcrumbs(breadcrumbPath);
}
}, [breadcrumbPath, updateBreadcrumbs]);
// 当文档加载完成且 progress === 1 时,加载分块列表
// Load chunk list when document is loaded and progress === 1
useEffect(() => {
if (document && document.progress === 1 && !isManualRefreshRef.current) {
ChunkList();
}
// 重置标志
// Reset flag
isManualRefreshRef.current = false;
}, [document]);
// 监听 keywords 变化,重新搜索
// Listen to keywords changes and re-search
useEffect(() => {
if (documentId && keywords && document?.progress === 1) {
setPage(1); // 重置页码
setChunkList([]); // 清空列表
ChunkList(1, false); // 重新加载第一页
setPage(1); // Reset page number
setChunkList([]); // Clear list
ChunkList(1, false); // Reload first page
}
}, [keywords]);
@@ -129,9 +129,9 @@ const DocumentDetails: FC = () => {
const url = `${imagePath}/api/files/${response.file_id}`
setFileUrl(url);
setParserMode(response?.parser_config?.auto_questions || 0)
// ChunkList 会在 useEffect 中根据 document.progress 自动调用
// ChunkList will be called automatically in useEffect based on document.progress
} catch (error) {
console.error('获取文档详情失败:', error);
console.error('Failed to fetch document details:', error);
message.error(t('common.loadFailed') || '加载失败');
} finally {
setLoading(false);
@@ -140,12 +140,12 @@ const DocumentDetails: FC = () => {
const ChunkList = async (pageNum: number = 1, append: boolean = false, force: boolean = false) => {
if (!documentId) return;
// 如果不是强制刷新,且正在加载中,则跳过
// Skip if not force refresh and already loading
if (!force && chunkLoading) {
return;
}
// 只有当文档处理完成时才获取分块列表
// Only fetch chunk list when document processing is complete
if (document && document.progress !== 1) {
return;
}
@@ -157,10 +157,10 @@ const DocumentDetails: FC = () => {
keywords: keywords || undefined,
page: pageNum,
pagesize: 20,
_t: force ? Date.now() : undefined, // 强制刷新时添加时间戳破坏缓存
_t: force ? Date.now() : undefined, // Add timestamp to break cache when force refresh
});
// 转换数据格式以匹配 RecallTestData
// Convert data format to match RecallTestData
const formattedChunks: RecallTestData[] = response.items.map((item: any) => ({
page_content: item.page_content || item.content || '',
vector: null,
@@ -172,7 +172,7 @@ const DocumentDetails: FC = () => {
document_id: item.metadata.document_id || documentId || '',
knowledge_id: item.metadata.knowledge_id || knowledgeBaseId || '',
sort_id: item.metadata.sort_id || item.id || 0,
score: item.metadata.score || null, // chunk 列表没有相似度分数
score: item.metadata.score || null, // Chunk list has no similarity score
status: item.metadata.status,
},
children: null,
@@ -186,7 +186,7 @@ const DocumentDetails: FC = () => {
setHasMore(response.page?.has_next ?? false);
} catch (error) {
console.error('获取文档详情失败:', error);
console.error('Failed to fetch document details:', error);
message.error(t('common.loadFailed') || '加载失败');
} finally {
setChunkLoading(false);
@@ -201,17 +201,17 @@ const DocumentDetails: FC = () => {
const handleBack = () => {
if (knowledgeBaseId && breadcrumbPath) {
// 返回到知识库详情页,并传递面包屑信息以恢复状态
// Return to knowledge base detail page and pass breadcrumb info to restore state
const navigationState = {
fromKnowledgeBaseList: true,
knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath,
navigateToDocumentFolder: locationParentId,
documentFolderPath: breadcrumbPath.documentFolderPath,
timestamp: Date.now(), // 添加时间戳确保状态变化
timestamp: Date.now(), // Add timestamp to ensure state change
};
navigate(`/knowledge-base/${knowledgeBaseId}/private`, { state: navigationState });
} else if (knowledgeBaseId) {
// 降级处理:直接跳转到知识库详情页
// Fallback: Navigate directly to knowledge base detail page
navigate(`/knowledge-base/${knowledgeBaseId}/private`);
}
};
@@ -226,61 +226,61 @@ const DocumentDetails: FC = () => {
insertModalRef.current?.handleOpen(documentId);
};
// 处理插入/编辑内容
// Handle insert/edit content
const handleInsertContent = async (_docId: string, content: string, chunkId?: string): Promise<boolean> => {
try {
if (chunkId) {
// 编辑模式:更新现有块
// Edit mode: Update existing chunk
const response = await updateDocumentChunk(knowledgeBaseId || '', documentId, chunkId, { content });
// 直接更新前端列表,不等待后端缓存刷新
// Update frontend list directly without waiting for backend cache refresh
setChunkList(prev => prev.map(item =>
item.metadata?.doc_id === chunkId
? { ...item, page_content: response.page_content || content }
: item
));
// 编辑模式返回特殊标记,告诉 InsertModal 不要调用 onSuccess
// Edit mode returns special flag to tell InsertModal not to call onSuccess
return true;
} else {
// 插入模式:创建新块
// Insert mode: Create new chunk
await createDocumentChunk(knowledgeBaseId || '', documentId, { content });
return true;
}
} catch (error) {
console.error('操作失败:', error);
console.error('Operation failed:', error);
return false;
}
};
// 处理点击文本块
// Handle click on text chunk
const handleChunkClick = (item: RecallTestData, index: number) => {
if (!documentId) return;
const chunkId = String(item.metadata?.doc_id || index);
insertModalRef.current?.handleOpen(documentId, item.page_content, chunkId);
};
// 插入成功后的回调(仅用于插入新块,编辑操作已在 handleInsertContent 中同步更新)
// Callback after successful insert (only for inserting new chunks, edit operations are already updated synchronously in handleInsertContent)
const handleInsertSuccess = () => {
// 设置手动刷新标志,防止 useEffect 重复调用
// Set manual refresh flag to prevent useEffect from calling repeatedly
isManualRefreshRef.current = true;
// 重置页码
// Reset page number
setPage(1);
// 等待后端处理完成,然后重新加载数据(仅用于插入新块的情况)
// Wait for backend processing to complete, then reload data (only for inserting new chunks)
setTimeout(() => {
ChunkList(1, false, true).then(() => {
return fetchDocumentDetail();
}).catch(err => {
console.error('刷新失败:', err);
console.error('Refresh failed:', err);
});
}, 1000);
};
const handleAdjustmentParameter = () =>{
if (!knowledgeBaseId || !document) return;
const targetFileId = document.id;
// 优先使用从 location 传递的 parentId其次使用 document.parent_id,最后使用 knowledgeBaseId
// Prioritize parentId from location, then document.parent_id, finally knowledgeBaseId
const parentId = locationParentId ?? document.parent_id ?? document.kb_id ?? knowledgeBaseId;
navigate(`/knowledge-base/${knowledgeBaseId}/create-dataset`, {
@@ -317,7 +317,7 @@ const DocumentDetails: FC = () => {
<img src={exitIcon} alt='exit' className='rb:w-4 rb:h-4' />
<span className='rb:text-gray-500 rb:text-sm'>{t('common.exit')}</span>
</div>
{/* 文档预览 */}
{/* Document preview */}
{fileUrl && (
<div className='rb:flex-1 rb:border rb:border-[#DFE4ED] rb:bg-white rb:rounded-xl rb:p-4 rb:overflow-hidden'>
<h3 className="rb:text-sm rb:font-medium rb:mb-3">
@@ -339,7 +339,7 @@ const DocumentDetails: FC = () => {
return (<>
<div className="rb:flex rb:flex-col rb:h-full rb:p-4">
{/* 头部 */}
{/* Header */}
<div className="rb:flex rb:flex-col rb:text-left rb:mb-6">
<div className='rb:flex rb:items-center rb:justify-between'>
<div className='rb:flex rb:items-center rb:gap-2 rb:mb-4 rb:cursor-pointer' onClick={handleBack}>
@@ -366,9 +366,9 @@ const DocumentDetails: FC = () => {
</div>
</div>
{/* 内容区域 */}
{/* Content area */}
<div className="rb:flex rb:h-full rb:gap-4 rb:flex-1 rb:overflow-hidden">
{/* 左侧:文档信息 */}
{/* Left: Document info */}
<div className='rb:w-80 rb:h-full rb:flex rb:flex-col rb:gap-4 rb:overflow-hidden'>
<div className='rb:border rb:border-[#DFE4ED] rb:bg-white rb:rounded-xl rb:p-4'>
<InfoPanel
@@ -381,7 +381,7 @@ const DocumentDetails: FC = () => {
</div>
</div>
{/* 右侧:分块列表 */}
{/* Right: Chunk list */}
<div
id="chunkScrollableDiv"
className="rb:flex-1 rb:bg-white rb:rounded-lg rb:border rb:border-gray-200 rb:p-6 rb:overflow-y-auto"
@@ -404,7 +404,7 @@ const DocumentDetails: FC = () => {
</div>
</div>
{/* 插入内容弹窗 */}
{/* Insert content modal */}
<InsertModal
ref={insertModalRef}
onInsert={handleInsertContent}

View File

@@ -40,7 +40,7 @@ import KnowledgeGraphCard from '../components/KnowledgeGraphCard';
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
import './Private.css'
const { confirm } = Modal
// 树节点数据类型
// Tree node data type
const Private: FC = () => {
const { t } = useTranslation();
@@ -73,9 +73,9 @@ const Private: FC = () => {
const [isGraph, setIsGraph] = useState(false);
const { updateBreadcrumbs } = useBreadcrumbManager({
breadcrumbType: 'detail',
// 不提供 onKnowledgeBaseMenuClick,让它使用默认的导航行为(返回列表页面)
// Don't provide onKnowledgeBaseMenuClick, let it use default navigation behavior (return to list page)
onKnowledgeBaseFolderClick: useCallback((folderId: string, folderPath: Array<{ id: string; name: string }>) => {
// 点击文件夹面包屑时,导航到对应文件夹
// Navigate to corresponding folder when clicking folder breadcrumb
setParentId(folderId);
setFolderPath(folderPath);
setSelectedKeys([folderId]);
@@ -84,7 +84,7 @@ const Private: FC = () => {
parent_id: folderId
});
// 确保query对象发生变化触发表格刷新
// Ensure query object changes to trigger table refresh
setQuery({
orderby: 'created_at',
desc: true,
@@ -92,10 +92,10 @@ const Private: FC = () => {
_timestamp: Date.now()
});
// 确保API URL正确设置
// Ensure API URL is set correctly
setTableApi(`/documents/${knowledgeBaseId}/documents`);
// 手动触发表格刷新,确保数据更新
// Manually trigger table refresh to ensure data update
setTimeout(() => {
tableRef.current?.loadData();
}, 100);
@@ -108,7 +108,7 @@ const Private: FC = () => {
setLoading(true);
try {
const res = await getKnowledgeBaseDetail(id);
// KnowledgeBase 转换为 KnowledgeBaseListItem
// Convert KnowledgeBase to KnowledgeBaseListItem
const listItem = res as unknown as KnowledgeBaseListItem;
setKnowledgeBase(listItem);
} finally {
@@ -122,7 +122,7 @@ const Private: FC = () => {
setTableApi(url);
fetchKnowledgeBaseDetail(knowledgeBaseId);
// 立即设置基础面包屑,确保不会显示其他页面的面包屑
// Immediately set base breadcrumbs to ensure other page breadcrumbs are not displayed
updateBreadcrumbs({
knowledgeBaseFolderPath,
knowledgeBase: {
@@ -135,7 +135,7 @@ const Private: FC = () => {
}
}, [knowledgeBaseId]);
// 更新面包屑
// Update breadcrumbs
useEffect(() => {
if (knowledgeBase) {
updateBreadcrumbs({
@@ -150,22 +150,22 @@ const Private: FC = () => {
}
}, [knowledgeBase, knowledgeBaseFolderPath, folderPath, updateBreadcrumbs]);
// 监听 tableApi 变化,自动刷新表格数据
// Listen to tableApi changes and auto refresh table data
useEffect(() => {
if (tableApi) {
tableRef.current?.loadData();
}
}, [tableApi]);
// 监听 query 变化,确保表格数据更新
// Listen to query changes and ensure table data update
useEffect(() => {
if (tableApi && query._timestamp) {
// query 中有 _timestamp 时,说明是通过面包屑或其他方式触发的更新
// When query has _timestamp, it means the update is triggered by breadcrumb or other means
tableRef.current?.loadData();
}
}, [query._timestamp, tableApi]);
// 监听 location state 变化
// Listen to location state changes
useEffect(() => {
const state = location.state as {
refresh?: boolean;
@@ -180,18 +180,18 @@ const Private: FC = () => {
if (state?.refresh) {
tableRef.current?.loadData();
// 清除 state,避免重复刷新
// Clear state to avoid repeated refresh
navigate(location.pathname, { replace: true, state: {} });
}
// 如果是从知识库列表页跳转过来的,设置知识库文件夹路径
// If navigated from knowledge base list page, set knowledge base folder path
if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) {
setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath);
}
// 如果需要重置到根目录(回到初始状态)
// If need to reset to root directory (return to initial state)
if (state?.resetToRoot) {
// 重置所有状态到初始状态,和页面初始化保持一致
// Reset all states to initial state, consistent with page initialization
setParentId(knowledgeBaseId);
setFolderPath([]);
setSelectedKeys([]);
@@ -202,31 +202,31 @@ const Private: FC = () => {
setQuery({
orderby: 'created_at',
desc: true,
_timestamp: Date.now() // 添加时间戳确保query对象发生变化触发API调用
_timestamp: Date.now() // Add timestamp to ensure query object changes and trigger API call
});
// 重新设置API URL
// Reset API URL
const rootUrl = `/documents/${knowledgeBaseId}/documents`;
setTableApi(rootUrl);
// 清除自动展开路径
// Clear auto expand path
setAutoExpandPath([]);
// 刷新文件夹树 - 使用延迟确保状态重置完成后再刷新
// Refresh folder tree - use delay to ensure state reset is complete before refresh
setTimeout(() => {
setFolderTreeRefreshKey((prev) => prev + 1);
}, 100);
// 手动触发表格刷新,确保数据更新
// Manually trigger table refresh to ensure data update
setTimeout(() => {
tableRef.current?.loadData();
}, 200);
// 清除 state,避免重复处理
// Clear state to avoid repeated processing
navigate(location.pathname, { replace: true, state: {} });
}
// 如果是从文档详情页返回,恢复文档文件夹路径
// If returning from document details page, restore document folder path
if (state?.navigateToDocumentFolder && state?.documentFolderPath) {
setFolderPath(state.documentFolderPath);
setParentId(state.navigateToDocumentFolder);
@@ -242,25 +242,25 @@ const Private: FC = () => {
setTableApi(`/documents/${knowledgeBaseId}/documents`);
setSelectedKeys([state.navigateToDocumentFolder]);
// 设置自动展开路径让FolderTree自动展开到对应位置
// Set auto expand path to let FolderTree auto expand to corresponding position
setAutoExpandPath(state.documentFolderPath);
// 手动触发表格刷新
// Manually trigger table refresh
setTimeout(() => {
tableRef.current?.loadData();
}, 100);
// 清除自动展开路径避免重复触发延迟清除确保FolderTree处理完成
// Clear auto expand path to avoid repeated trigger (delayed clear to ensure FolderTree processing is complete)
setTimeout(() => {
setAutoExpandPath([]);
}, 2000);
}
}, [location.state, knowledgeBaseId, navigate, location.pathname]);
// 处理树节点选择
// Handle tree node selection
const onSelect = (keys: React.Key[]) => {
if (!keys.length) {
// 如果没有选中任何节点,回到根目录(初始状态)
// If no node is selected, return to root directory (initial state)
setParentId(knowledgeBaseId);
setFolder({
kb_id: knowledgeBaseId ?? '',
@@ -269,7 +269,7 @@ const Private: FC = () => {
setQuery({
orderby: 'created_at',
desc: true,
_timestamp: Date.now() // 添加时间戳确保query对象发生变化
_timestamp: Date.now() // Add timestamp to ensure query object changes
});
setSelectedKeys([]);
return;
@@ -284,7 +284,7 @@ const Private: FC = () => {
setQuery({
...query,
parent_id: String(keys[0]),
_timestamp: Date.now() // 添加时间戳确保query对象发生变化
_timestamp: Date.now() // Add timestamp to ensure query object changes
})
let url = `/documents/${knowledgeBaseId}/documents`;
@@ -294,14 +294,14 @@ const Private: FC = () => {
setSelectedKeys(keys)
};
// 处理文件夹路径变化
// Handle folder path change
const handleFolderPathChange = (path: Array<{ id: string; name: string }>) => {
setFolderPath(path);
};
// 处理树节点展开
// Handle tree node expand
const onExpand = (_expandedKeys: React.Key[], _info: any) => {
// 展开节点时不需要特殊处理
// No special handling needed when expanding nodes
};
// create / import list
const createItems: MenuProps['items'] = [
@@ -344,13 +344,13 @@ const Private: FC = () => {
// createImageDataset?.current?.handleOpen(knowledgeBaseId || '', parentId || '')
// },
// },
// 暂时未实现
// Not implemented yet
// {
// key: '4',
// icon: <img src={blankIcon} alt="blank" style={{ width: 16, height: 16 }} />,
// label: t('knowledgeBase.blankDataset'),
// onClick: () => {
// handleCreate('folder'); // 传入 type: 'folder'
// handleCreate('folder'); // Pass type: 'folder'
// },
// },
// {
@@ -362,7 +362,7 @@ const Private: FC = () => {
// icon: <img src={templateIcon} alt="import" style={{ width: 16, height: 16 }} />,
// label: t('knowledgeBase.importTemplate'),
// onClick: () => {
// handleCreate('folder'); // 传入 type: 'folder'
// handleCreate('folder'); // Pass type: 'folder'
// },
// },
// {
@@ -370,17 +370,17 @@ const Private: FC = () => {
// icon: <img src={backupIcon} alt="import" style={{ width: 16, height: 16 }} />,
// label: t('knowledgeBase.importBackup'),
// onClick: () => {
// handleCreate('folder'); // 传入 type: 'folder'
// handleCreate('folder'); // Pass type: 'folder'
// },
// },
];
// 处理开关
// Handle switch
const onChange = (checked: boolean) => {
if (!knowledgeBase) return;
// 构造完整的更新数据,保留现有配置
// Construct complete update data, keeping existing configuration
const updateData: KnowledgeBaseFormData = {
name: knowledgeBase.name,
description: knowledgeBase.description,
@@ -411,30 +411,30 @@ const Private: FC = () => {
updateKnowledgeBase(knowledgeBaseId || '', updateData);
console.log(`switch to ${checked}`);
};
// 处理搜索
// Handle search
const handleSearch = (value?: string) => {
setQuery({ ...query, keywords: value })
}
// 处理分享
// Handle share
const handleShare = () => {
shareModalRef?.current?.handleOpen(knowledgeBaseId,knowledgeBase);
}
// 处理分享回调,接收选中的数据
// Handle share callback, receive selected data
const handleShareCallback = (selectedData: { checkedItems: any[], selectedItem: any | null }) => {
console.log('选中的数据:', selectedData);
// checkedItems: 所有 checked true 的数据
// selectedItem: 当前选中的项(curIndex 对应的数据)
// 在这里处理分享逻辑
console.log('Selected data:', selectedData);
// checkedItems: All data with checked = true
// selectedItem: Currently selected item (corresponding to curIndex)
// Handle share logic here
}
const handleCreateDatasetCallback = (payload: { value: number; title: string; description: string }) => {
console.log('创建数据集:', payload);
console.log('Create dataset:', payload);
}
// 处理设置
// Handle settings
const handleSetting = () => {
modalRef?.current?.handleOpen(knowledgeBase, '');
}
// 处理召回测试
// Handle recall test
const handleRecallTest = () => {
recallTestDrawerRef?.current?.handleOpen(knowledgeBaseId);
}
@@ -443,7 +443,7 @@ const Private: FC = () => {
const handelCreateOrImport = () => {
}
// 生成下拉菜单项(根据当前 row
// Generate dropdown menu items (based on current row)
const getOptMenuItems = (row: KnowledgeBaseListItem): MenuProps['items'] => [
{
key: '1',
@@ -495,19 +495,19 @@ const Private: FC = () => {
deleteDocument(item.id)
.then(() => {
messageApi.success(t('common.deleteSuccess'));
// 刷新表格数据
// Refresh table data
tableRef.current?.loadData();
})
.catch((err: any) => {
console.log('删除失败', err);
console.log('Delete failed', err);
});
},
onCancel: () => {
console.log('取消删除');
console.log('Cancel delete');
},
});
}
// 表格列配置
// Table column configuration
const columns: ColumnsType = [
{
title: t('knowledgeBase.name'),
@@ -524,7 +524,7 @@ const Private: FC = () => {
state: {
documentId: document.id,
parentId: parentId ?? knowledgeBaseId,
// 传递面包屑信息
// Pass breadcrumb information
breadcrumbPath: {
knowledgeBaseFolderPath,
knowledgeBase: {
@@ -572,7 +572,7 @@ const Private: FC = () => {
render: (value: string) => {
if (!value) return '-';
// 解析日志格式,将 \n 转换为换行
// Parse log format, convert \n to newline
const formattedText = value.replace(/\\n/g, '\n');
return (
@@ -634,25 +634,25 @@ const Private: FC = () => {
),
},
];
// 刷新列表数据
// Refresh list data
if (loading) {
return <div>...</div>;
return <div>Loading...</div>;
}
if (!knowledgeBase) {
return <div></div>;
}
const refreshDirectoryTree = async () => {
// 先刷新知识库详情,确保数据是最新的
// First refresh knowledge base details to ensure data is up-to-date
if (knowledgeBase?.id) {
await fetchKnowledgeBaseDetail(knowledgeBase.id);
}
// 添加短暂延迟,确保后端数据已经完全更新
// Add short delay to ensure backend data is fully updated
await new Promise(resolve => setTimeout(resolve, 300));
// 然后刷新文件夹树
// Then refresh folder tree
setFolderTreeRefreshKey((prev) => prev + 1);
// 确保 folder 状态正确设置
// Ensure folder state is set correctly
if (!folder) {
setFolder({
kb_id: knowledgeBaseId ?? '',
@@ -663,10 +663,10 @@ const Private: FC = () => {
}
const handleRootTreeLoad = (nodes: TreeNodeData[] | null) => {
if (!nodes || nodes.length === 0) {
// 如果没有节点设置folder为null这会隐藏FolderTree
// If no nodes, set folder to null (this will hide FolderTree)
setFolder(null);
} else {
// 如果有节点且 folder null,重新设置 folder
// If there are nodes and folder is null, reset folder
if (!folder) {
setFolder({
kb_id: knowledgeBaseId ?? '',
@@ -687,7 +687,7 @@ const Private: FC = () => {
}
const handleRefreshTable = () => {
// 刷新表格数据
// Refresh table data
fetchKnowledgeBaseDetail(knowledgeBase.id)
tableRef.current?.loadData();
}

View File

@@ -38,7 +38,7 @@ const Share: FC = () => {
if (knowledgeBaseId) {
fetchKnowledgeBaseDetail(knowledgeBaseId);
// 打开召回测试组件
// Open recall test component
setTimeout(() => {
console.log('Share.tsx - calling handleOpen with:', knowledgeBaseId);
recallTestRef.current?.handleOpen(knowledgeBaseId);
@@ -48,7 +48,7 @@ const Share: FC = () => {
}
}, [knowledgeBaseId]);
// 更新面包屑
// Update breadcrumbs
useEffect(() => {
if (knowledgeBase) {
updateBreadcrumbs({
@@ -63,14 +63,14 @@ const Share: FC = () => {
}
}, [knowledgeBase, knowledgeBaseFolderPath, updateBreadcrumbs]);
// 监听 location state 变化
// Listen to location state changes
useEffect(() => {
const state = location.state as {
fromKnowledgeBaseList?: boolean;
knowledgeBaseFolderPath?: BreadcrumbItem[];
} | null;
// 如果是从知识库列表页跳转过来的,设置知识库文件夹路径
// If navigated from knowledge base list page, set knowledge base folder path
if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) {
setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath);
}

View File

@@ -41,7 +41,7 @@ const CreateContentModal = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
const values = await form.validateFields();
setLoading(true);
// TODO: 这里需要调用相应的API来保存内容
// TODO: Call appropriate API to save content
const params = {
// ...values,
kb_id: kbId,
@@ -55,7 +55,7 @@ const CreateContentModal = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
}
handleClose();
} catch (err) {
console.error('创建内容失败:', err);
console.error('Failed to create content:', err);
} finally {
setLoading(false);
}

View File

@@ -3,18 +3,18 @@ import { Button } from 'antd';
import CreateContentModal from './CreateContentModal';
import type { CreateContentModalRef } from '../types';
// 使用示例组件
// Example usage component
const CreateContentModalExample = () => {
const createContentModalRef = useRef<CreateContentModalRef>(null);
const handleOpenModal = () => {
// 打开弹窗传入知识库ID和父级ID
// Open modal, pass knowledge base ID and parent ID
createContentModalRef.current?.handleOpen('kb_123', 'parent_456');
};
const handleRefreshTable = () => {
console.log('刷新表格数据');
// 这里可以添加刷新表格的逻辑
console.log('Refresh table data');
// Add table refresh logic here
};
return (

View File

@@ -1,5 +1,5 @@
/*
* @Description:
/**
* @Description: Create Dataset Modal
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2025-11-10 18:52:55

View File

@@ -13,7 +13,7 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
const [form] = Form.useForm<FolderFormData>();
const [loading, setLoading] = useState(false)
// 封装取消方法,添加关闭弹窗逻辑
// Close modal and reset state
const handleClose = () => {
setFolder({} as FolderFormData);
form.resetFields();
@@ -22,17 +22,16 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
};
const handleOpen = (folder?: FolderFormData | null) => {
debugger
if (folder) {
setFolder(folder);
// 设置表单值
// Set form values
form.setFieldsValue({
folder_name: folder.folder_name,
parent_id: folder.parent_id ?? '',
kb_id: folder.kb_id ?? '',
});
} else {
// 新建时,重置表单并设置默认值
// Reset form and set default values for new folder
form.resetFields();
form.setFieldsValue({
parent_id: '',
@@ -41,7 +40,7 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
}
setVisible(true);
};
// 封装保存方法,添加提交逻辑
// Save form data and submit
const handleSave = () => {
form
.validateFields({ validateOnly: true })
@@ -74,13 +73,13 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
});
}
// 暴露给父组件的方法
// Expose methods to parent component
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
// 根据 type 获取标题
// Get modal title based on folder state
const getTitle = () => {
if (folder.id) {
return t('common.edit') + ' ' + (folder.folder_name || '');

View File

@@ -28,12 +28,12 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
const [parentId, setParentId] = useState<string>('');
const [hasFiles, setHasFiles] = useState(false);
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
// 存储每个文件的 AbortController,用于取消上传
// Store AbortController for each file to cancel upload
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
// const fileIds = [];
const handleClose = () => {
// 取消所有正在进行的上传
// Cancel all ongoing uploads
abortControllersRef.current.forEach((controller) => {
controller.abort();
});
@@ -69,7 +69,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
}
const ids = fileList.map((file) => file.response?.id);
handleChunking(kbId, parentId, ids)
// // 上传所有图片
// // Upload all images
// const uploadPromises = fileList.map(async (file) => {
// if (file.originFileObj) {
// const formData = new FormData();
@@ -91,7 +91,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
handleClose();
} catch (err) {
console.error('创建图片数据集失败:', err);
console.error('Failed to create image dataset:', err);
} finally {
setLoading(false);
}
@@ -112,7 +112,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
useImperativeHandle(ref, () => ({
handleOpen,
}));
// 检查媒体文件时长的辅助函数
// Helper function to check media file duration
const checkMediaDuration = (file: File): Promise<number> => {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
@@ -131,7 +131,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
media.src = url;
});
};
// 删除已上传的文件
// Delete uploaded file
const handleDeleteFile = async (fileId: string) => {
try {
await deleteDocument(fileId);
@@ -141,24 +141,24 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
}
};
// 上传文件
// Upload file
const handleUpload = async (options: UploadRequestOption) => {
const { file, onSuccess, onError, onProgress, filename = 'file' } = options;
// 创建 AbortController 用于取消上传
// Create AbortController to cancel upload
const abortController = new AbortController();
const fileUid = (file as any).uid;
abortControllersRef.current.set(fileUid, abortController);
// 获取文件扩展名
// Get file extension
const fileExtension = (file as File).name.split('.').pop()?.toLowerCase();
const mediaExtensions = ['mp3', 'mp4', 'mov', 'wav'];
// 如果是媒体文件,进行大小和时长检查
// If it's a media file, check size and duration
if (fileExtension && mediaExtensions.includes(fileExtension)) {
const fileSizeInMB = (file as File).size / (50 * 1024);
// 检查文件大小50MB限制
// Check file size (50MB limit)
if (fileSizeInMB > 50) {
messageApi.error(`${t('knowledgeBase.sizeLimitError')}${fileSizeInMB.toFixed(2)}MB`);
onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`));
@@ -167,7 +167,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
}
try {
// 检查媒体时长150秒限制
// Check media duration (150 seconds limit)
const duration = await checkMediaDuration(file as File);
if (duration > 150) {
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}${Math.round(duration)}`);
@@ -204,21 +204,21 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
},
});
// 上传成功,移除 AbortController
// Upload successful, remove AbortController
abortControllersRef.current.delete(fileUid);
onSuccess?.(res, new XMLHttpRequest());
if (res?.id) {
// 上传成功
// Upload successful
// fileIds.push(res.id)
}
} catch (error: any) {
// 移除 AbortController
// Remove AbortController
abortControllersRef.current.delete(fileUid);
// 如果是用户主动取消,不显示错误信息
// If user actively cancelled, don't show error message
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
console.log('上传已取消:', (file as File).name);
console.log('Upload cancelled:', (file as File).name);
return;
}
@@ -259,11 +259,11 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp3', 'mp4', 'mov', 'wav']}
customRequest={handleUpload}
onChange={(fileList) => {
// 实时更新文件状态
// Update file status in real-time
setHasFiles(fileList.length > 0);
}}
onRemove={async (file) => {
// 如果文件正在上传,取消上传
// If file is uploading, cancel upload
const fileUid = file.uid;
const abortController = abortControllersRef.current.get(fileUid);
if (abortController) {
@@ -271,12 +271,12 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
abortControllersRef.current.delete(fileUid);
}
// 如果文件已经上传成功,删除服务器上的文件
// If file is already uploaded successfully, delete file on server
if (file.response?.id) {
await handleDeleteFile(file.response.id);
}
return true; // 允许移除文件
return true; // Allow file removal
}}
/>
</Form.Item>

View File

@@ -15,7 +15,7 @@ import RbModal from '@/components/RbModal'
const { TextArea } = Input;
const { confirm } = Modal
// 全局模型数据常量
// Global model data constant
let models: any = null;
const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
@@ -33,9 +33,9 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
const [activeTab, setActiveTab] = useState('basic');
const [generatingEntityTypes, setGeneratingEntityTypes] = useState(false);
const [isRebuildMode, setIsRebuildMode] = useState(false);
const [originalType, setOriginalType] = useState<string>(''); // 保存原始的 type 参数
const [originalType, setOriginalType] = useState<string>(''); // Save original type parameter
// 监听 parser_config.graphrag 相关字段的变化
// Watch for changes to parser_config.graphrag related fields
const parserConfig = Form.useWatch('parser_config', form);
const graphragConfig = parserConfig?.graphrag;
const enableKnowledgeGraph = graphragConfig?.use_graphrag || false;
@@ -43,30 +43,30 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
const entityNormalization = graphragConfig?.resolution || false;
const communityReportGeneration = graphragConfig?.community || false;
// 封装取消方法,添加关闭弹窗逻辑
// Encapsulate cancel method, add close modal logic
const handleClose = () => {
setDatasets(null);
form.resetFields();
setLoading(false);
setActiveTab('basic');
setIsRebuildMode(false); // 重置重建模式标识
setOriginalType(''); // 重置原始 type
setIsRebuildMode(false); // Reset rebuild mode flag
setOriginalType(''); // Reset original type
setVisible(false);
};
// 生成实体类型的函数
// Generate entity types function
const generateEntityTypes = async () => {
const sceneName = form.getFieldValue(['parser_config', 'graphrag', 'scene_name']);
if (!sceneName) {
// 可以添加提示用户输入场景名称
// Can add prompt for user to enter scenario name
messageApi.error(t('knowledgeBase.enterScenarioName'));
return;
}
// 检查是否选择了 LLM 模型
// Check if LLM model is selected
const llmId = form.getFieldValue('llm_id');
if (!llmId) {
// 跳转到基础配置页
// Navigate to basic configuration page
setActiveTab('basic');
messageApi.error(t('knowledgeBase.pleaseSelectLLMModel'));
return;
@@ -74,7 +74,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
setGeneratingEntityTypes(true);
try {
// 这里应该调用实际的API接口
// Call the actual API interface here
// const user = JSON.parse(localStorage.getItem('user') as any);
//datasets?.id || datasets?.parent_id || user?.current_workspace_id,
const params = {
@@ -82,17 +82,17 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
llm_id: llmId
};
const response = await getKnowledgeGraphEntityTypes(params);
// 模拟API调用
// Simulate API call
// await new Promise(resolve => setTimeout(resolve, 1000));
// 处理API响应数据
console.log('API Response:', response); // 调试日志
// Process API response data
console.log('API Response:', response); // Debug log
// 检查响应结构 - API直接返回字符串
// Check response structure - API returns string directly
if (response && typeof response === 'string' && response.trim()) {
// 将逗号分隔的字符串转换为换行分隔的格式以便在TextArea中显示
// Convert comma-separated string to newline-separated format for TextArea display
const entityTypesString = response.replace(/,\s*/g, '\n');
console.log('Converted entity types:', entityTypesString); // 调试日志
console.log('Converted entity types:', entityTypesString); // Debug log
const currentGraphrag = form.getFieldValue(['parser_config', 'graphrag']) || {};
const updatedGraphrag = {
@@ -100,22 +100,22 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
entity_types: entityTypesString
};
console.log('Updating form with:', updatedGraphrag); // 调试日志
console.log('Updating form with:', updatedGraphrag); // Debug log
// 使用更直接的方式更新表单字段
// Use more direct way to update form field
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
// 强制触发表单重新渲染
// Force trigger form re-render
form.validateFields([['parser_config', 'graphrag', 'entity_types']]);
// 额外的强制更新机制
// Additional forced update mechanism
setTimeout(() => {
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
}, 100);
messageApi.success(t('knowledgeBase.generateEntityTypesSuccess'));
} else {
messageApi.error(t('knowledgeBase.generateEntityTypesFailed') + '' + t('knowledgeBase.unknownError'));
messageApi.error(t('knowledgeBase.generateEntityTypesFailed') + ': ' + t('knowledgeBase.unknownError'));
}
} catch (error) {
console.error(t('knowledgeBase.generateEntityTypesFailed') + ':', error);
@@ -143,7 +143,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
};
const fetchModelLists = async (types: string[]) => {
// 如果还没有获取过全部模型数据,则获取一次
// If model data hasn't been fetched yet, fetch it once
if (!models) {
try {
models = await getModelList({ page: 1, pagesize: 100 });
@@ -153,7 +153,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
}
}
// 从全部模型数据中过滤出需要的类型
// Filter out the required types from all model data
const typesToFetch = types.includes('llm') ? [...types, 'chat'] : types;
const next: Record<string, { label: string; value: string }[]> = {};
@@ -165,7 +165,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
setModelOptionsByType(next);
// 如果不是编辑模式,为每个类型的下拉框设置默认值为第一条数据
// If not in edit mode, set default value to first item for each type dropdown
if (!datasets?.id) {
const defaultValues: Record<string, string> = {};
types.forEach((tp) => {
@@ -174,7 +174,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
? [...(next['llm'] || []), ...(next['chat'] || [])]
: next[tp] || [];
// 如果有选项且当前字段没有值,设置第一个选项为默认值
// If there are options and current field has no value, set first option as default
if (options.length > 0 && !form.getFieldValue(fieldKey)) {
defaultValues[fieldKey] = options[0].value;
}
@@ -204,7 +204,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
status: record.status,
};
// 处理 parser_config 配置数据,如果没有则设置默认值
// Process parser_config data, set default values if not present
baseValues.parser_config = record.parser_config || {
graphrag: {
use_graphrag: false,
@@ -216,13 +216,13 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
}
};
// 如果存在 entity_types,转换为换行分隔格式用于 TextArea 显示
// If entity_types exists, convert to newline-separated format for TextArea display
if (baseValues.parser_config.graphrag.entity_types) {
if (Array.isArray(baseValues.parser_config.graphrag.entity_types)) {
// 如果是数组格式,转换为换行分隔字符串
// If array format, convert to newline-separated string
(baseValues.parser_config.graphrag as any).entity_types = baseValues.parser_config.graphrag.entity_types.join('\n');
} else if (typeof baseValues.parser_config.graphrag.entity_types === 'string') {
// 如果是逗号分隔字符串格式,转换为换行分隔字符串(兼容旧数据)
// If comma-separated string format, convert to newline-separated string (compatible with old data)
(baseValues.parser_config.graphrag as any).entity_types = (baseValues.parser_config.graphrag.entity_types as string).replace(/,\s*/g, '\n');
}
}
@@ -249,13 +249,13 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
const handleOpen = (record?: KnowledgeBaseListItem | null, type?: string) => {
setDatasets(record || null);
// 如果是重建模式,使用记录的实际类型,否则使用传入的类型
// If rebuild mode, use record's actual type, otherwise use passed type
const actualType = type === 'rebuild' ? (record?.type || 'General') : (type || currentType);
setCurrentType(actualType as any);
setIsRebuildMode(type === 'rebuild'); // 设置重建模式标识
setOriginalType(type || ''); // 保存原始的 type 参数
setIsRebuildMode(type === 'rebuild'); // Set rebuild mode flag
setOriginalType(type || ''); // Save original type parameter
// 如果是重建模式,默认切换到知识图谱标签页
// If rebuild mode, default to knowledge graph tab
if (type === 'rebuild') {
setActiveTab('knowledgeGraph');
} else {
@@ -285,13 +285,13 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
setDynamicModelFields(datasets, modelTypeList);
}, [visible, datasets, currentType, modelTypeList]);
// 封装保存方法,添加提交逻辑
// Encapsulate save method, add submit logic
const handleSave = () => {
// 获取当前表单中的知识图谱开启状态
// Get current knowledge graph enabled status from form
const currentFormValues = form.getFieldsValue();
const isGraphragEnabled = currentFormValues?.parser_config?.graphrag?.use_graphrag || false;
// 如果原始 type 'rebuild' 并且知识图谱开启为true显示确认弹框
// If original type is 'rebuild' and knowledge graph is enabled, show confirmation dialog
if (originalType === 'rebuild' && isGraphragEnabled) {
confirm({
title: t('knowledgeBase.rebuildConfirmTitle'),
@@ -302,11 +302,11 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
await rebuildKnowledgeGraph(datasets?.id || '')
},
onCancel: () => {
// 用户取消,不执行任何操作
// User cancelled, no action taken
},
});
} else {
// 非重建模式或知识图谱未开启,直接保存
// Non-rebuild mode or knowledge graph not enabled, save directly
performSave();
}
};
@@ -318,7 +318,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
messageApi.error(t('knowledgeBase.deleteGraphFailed'))
}
};
// 实际的保存逻辑
// Actual save logic
const performSave = () => {
form
.validateFields()
@@ -326,7 +326,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
setLoading(true)
const formValues = form.getFieldsValue();
// 处理 entity_types 格式转换:从换行分隔字符串转换为字符串数组
// Process entity_types format conversion: from newline-separated string to string array
if (formValues.parser_config && formValues.parser_config.graphrag && formValues.parser_config.graphrag.entity_types) {
const entityTypesString = formValues.parser_config.graphrag.entity_types as any as string;
const entityTypesArray = entityTypesString
@@ -336,7 +336,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
formValues.parser_config.graphrag.entity_types = entityTypesArray;
}
// 确保保存时使用正确的类型(不是 'rebuild'
// Ensure correct type is used when saving (not 'rebuild')
const saveType = originalType === 'rebuild' ? currentType : (formValues.type || currentType);
const payload: KnowledgeBaseFormData = {
@@ -346,7 +346,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
parent_id: datasets?.parent_id || undefined,
};
console.log('Saving payload:', payload); // 调试日志
console.log('Saving payload:', payload); // Debug log
const submit = datasets?.id
? updateKnowledgeBase(datasets.id, payload)
@@ -367,32 +367,32 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
});
}
const handleChange = (_value: string, tp: string) => {
// 只在编辑模式且类型为 embedding 时触发提示
// Only trigger prompt in edit mode and when type is embedding
if (datasets?.id && tp.toLowerCase() === 'embedding') {
const fieldKey = typeToFieldKey(tp);
// 从原始 datasets 对象中获取之前的值
// Get previous value from original datasets object
const previousValue = (datasets as any)[fieldKey];
confirm({
title: t('common.updateWarning'),
content: t('knowledgeBase.updateEmbeddingContent'),
onOk: () => {
// 确定时什么也不做,保持新值
// Do nothing on confirm, keep new value
},
onCancel: () => {
// 取消时恢复之前的值
// Restore previous value on cancel
form.setFieldsValue({ [fieldKey]: previousValue } as any);
},
});
}
}
// 暴露给父组件的方法
// Methods exposed to parent component
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
// 根据 type 获取标题
// Get title based on type
const getTitle = () => {
if (isRebuildMode) {
return t('knowledgeBase.rebuildGraph') + ' - ' + (datasets?.name || '');
@@ -408,7 +408,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
const dynamicTypeList = useMemo(() => modelTypeList.filter((tp) => (modelOptionsByType[tp] || []).length), [modelTypeList, modelOptionsByType]);
// 基础配置表单内容
// Basic configuration form content
const renderBasicConfig = () => (
<>
{!datasets?.id && (
@@ -426,7 +426,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
{currentType !== 'Folder' && dynamicTypeList.map((tp) => {
const fieldKey = typeToFieldKey(tp);
// tp 'llm' 时,合并 llm chat 的选项
// When tp is 'llm', merge llm and chat options
const options = tp.toLowerCase() === 'llm'
? [...(modelOptionsByType['llm'] || []), ...(modelOptionsByType['chat'] || [])]
: modelOptionsByType[tp] || [];
@@ -451,7 +451,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
</>
);
// 知识图谱配置表单内容
// Knowledge graph configuration form content
const renderKnowledgeGraphConfig = () => (
<>
<div className={`rb:flex rb:w-full rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
@@ -482,7 +482,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
<div className='rb:text-[#212332] rb:text-base rb:font-medium rb:mb-4'>
{t('knowledgeBase.graphConfig')}
</div>
{/* 场景名称 */}
{/* Scene name */}
<div className='rb:flex rb:items-center rb:gap-2'>
<Form.Item
name={['parser_config', 'graphrag', 'scene_name']}
@@ -506,7 +506,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
</div>
{/* 实体类型 */}
{/* Entity types */}
<Form.Item
name={['parser_config', 'graphrag', 'entity_types']}
label={t('knowledgeBase.entityTypes')}
@@ -517,7 +517,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
/>
</Form.Item>
{/* 实体归一化 */}
{/* Entity normalization */}
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
entityNormalization
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
@@ -541,7 +541,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
</div>
{/* 实体方法 */}
{/* Entity method */}
<Form.Item
name={['parser_config', 'graphrag', 'method']}
label={t('knowledgeBase.entityMethod')}
@@ -553,7 +553,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
</Radio.Group>
</Form.Item>
{/* 社区报告生成 */}
{/* Community report generation */}
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
communityReportGeneration
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
@@ -580,7 +580,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
</>
);
// Tabs 配置
// Tabs configuration
const tabItems = [
{
key: 'basic',
@@ -607,16 +607,16 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
form={form}
layout="vertical"
initialValues={{
permission_id: 'Private', // 设置 permission_id 的默认值
permission_id: 'Private', // Set default value for permission_id
type: currentType,
parser_config: {
graphrag: {
use_graphrag: false, // 默认不启用知识图谱
scene_name: '', // 场景名称
entity_types: '' as any, // 实体类型(界面上显示为字符串,保存时转为数组)
method: 'general', // 默认使用通用方法
resolution: false, // 默认不启用实体归一化
community: false, // 默认不生成社区报告
use_graphrag: false, // Default not to enable knowledge graph
scene_name: '', // Scene name
entity_types: '' as any, // Entity types (displayed as string in UI, converted to array when saving)
method: 'general', // Default to use general method
resolution: false, // Default not to enable entity normalization
community: false, // Default not to generate community reports
}
}
}}

View File

@@ -17,13 +17,13 @@ const DelimiterSelector: FC<DelimiterSelectorProps> = ({
className = '',
}) => {
const { t } = useTranslation();
// 默认值为空字符串(不设置)
// Default value is empty string (not set)
const [selectedValue, setSelectedValue] = useState<string>(value || '');
const [customValue, setCustomValue] = useState<string>('');
const [showCustomInput, setShowCustomInput] = useState(false);
useEffect(() => {
// 检查当前值是否为自定义值
// Check if current value is a custom delimiter
if (value && isCustomDelimiter(value) && value !== 'custom') {
setSelectedValue('custom');
setCustomValue(value);
@@ -39,15 +39,15 @@ const DelimiterSelector: FC<DelimiterSelectorProps> = ({
if (val === 'custom') {
setShowCustomInput(true);
// 如果已有自定义值,使用它;否则等待用户输入
// If custom value exists, use it; otherwise wait for user input
if (customValue) {
onChange?.(customValue);
} else {
// 自定义但还没输入值,暂不触发 onChange
// Custom selected but no value entered yet, don't trigger onChange
onChange?.(undefined);
}
} else if (val === '') {
// 选择"不设置"时,返回 undefined不传递该参数
// When "Not set" is selected, return undefined (don't pass this parameter)
setShowCustomInput(false);
onChange?.(undefined);
} else {
@@ -59,7 +59,7 @@ const DelimiterSelector: FC<DelimiterSelectorProps> = ({
const handleCustomInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setCustomValue(val);
// 只有当输入不为空时才触发 onChange
// Only trigger onChange when input is not empty
onChange?.(val || undefined);
};

View File

@@ -60,7 +60,7 @@ interface FolderTreeProps {
onRootLoad?: (nodes: TreeNodeData[] | null) => void;
onFolderPathChange?: (path: Array<{ id: string; name: string }>) => void;
selectedKeys?: React.Key[];
// 新增:自动展开到指定路径
// New: Auto expand to specified path
autoExpandPath?: Array<{ id: string; name: string }>;
}
@@ -221,7 +221,7 @@ const extractItems = (resp: any): any[] => {
return [];
};
// 只加载当前层级的节点,不递归加载子节点
// Only load nodes at current level, don't recursively load child nodes
const buildTreeNodes = async (
kbId: string,
parentId: string,
@@ -229,7 +229,7 @@ const buildTreeNodes = async (
const currentParent = String(parentId ?? '');
if (!currentParent) return [];
// 只请求一次当前层级的数据,不分页
// Only request current level data once, no pagination
const response = await getFolderList({
kb_id: kbId,
parent_id: currentParent,
@@ -246,20 +246,20 @@ const buildTreeNodes = async (
const nodeKey = String(keySource);
const isFolder = isFolderLike(raw);
// 只显示文件夹
// Only show folders
if (!isFolder) {
continue;
}
// 文件夹节点初始不加载子节点isLeaf设为false表示可能有子节点
// Folder node initially doesn't load child nodes, isLeaf set to false indicates possible child nodes
nodes.push({
key: nodeKey,
title: getNodeTitle(raw),
icon: getNodeIcon(raw, isFolder),
switcherIcon: isFolder ? switcherIcon : undefined,
type: isFolder ? 'folder' : (typeof raw?.type === 'string' ? raw.type : normalizeExt(raw?.file_ext) || 'file'),
isLeaf: false, // 文件夹节点初始设为false表示可能有子节点需要展开时加载
children: undefined, // 初始不加载子节点
isLeaf: false, // Folder node initially set to false, indicating possible child nodes, load when expanded
children: undefined, // Initially don't load child nodes
});
}
@@ -283,7 +283,7 @@ const FolderTree: FC<FolderTreeProps> = ({
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [autoExpandInProgress, setAutoExpandInProgress] = useState(false);
// 更新树节点数据的辅助函数
// Helper function to update tree node data
const updateTreeData = (nodes: TreeNodeData[], key: Key, children: TreeNodeData[]): TreeNodeData[] => {
return nodes.map((node) => {
if (node.key === key) {
@@ -303,17 +303,17 @@ const FolderTree: FC<FolderTreeProps> = ({
});
};
// 加载根节点
// Load root nodes
useEffect(() => {
let cancelled = false;
const load = async () => {
if (!knowledgeBaseId) {
setTreeData([]);
setExpandedKeys([]); // 重置展开状态
setExpandedKeys([]); // Reset expand state
return;
}
try {
// 重置展开状态,确保从根目录开始
// Reset expand state, ensure starting from root directory
setExpandedKeys([]);
const nodes = await buildTreeNodes(knowledgeBaseId, knowledgeBaseId);
@@ -324,7 +324,7 @@ const FolderTree: FC<FolderTreeProps> = ({
}
}
} catch (e) {
console.error('加载文件夹树失败:', e);
console.error('Failed to load folder tree:', e);
if (!cancelled) {
const fallback = buildMockTreeData();
setTreeData(fallback);
@@ -340,27 +340,27 @@ const FolderTree: FC<FolderTreeProps> = ({
};
}, [knowledgeBaseId, refreshKey]);
// 懒加载子节点 - 只在展开时加载
// Lazy load child nodes - only load when expanded
const onLoadData = async (node: any) => {
const { key } = node;
// 如果已经加载过子节点,不再重复加载
// If child nodes already loaded, don't reload
if (node.children !== undefined) {
return Promise.resolve();
}
try {
// 使用节点的 key 作为 parent_id 加载子文件夹
// Use node's key as parent_id to load child folders
const children = await buildTreeNodes(knowledgeBaseId, String(key));
setTreeData((prevData) => updateTreeData(prevData, key, children));
} catch (e) {
console.error('加载子节点失败:', e);
// 加载失败时,将该节点标记为叶子节点(没有子节点)
console.error('Failed to load child nodes:', e);
// On load failure, mark this node as leaf node (no child nodes)
setTreeData((prevData) => updateTreeData(prevData, key, []));
}
};
// 查找节点路径的辅助函数
// Helper function to find node path
const findNodePath = (nodes: TreeNodeData[], targetKey: Key, currentPath: Array<{ id: string; name: string }> = []): Array<{ id: string; name: string }> | null => {
for (const node of nodes) {
const newPath = [...currentPath, { id: String(node.key), name: String(node.title) }];
@@ -379,7 +379,7 @@ const FolderTree: FC<FolderTreeProps> = ({
return null;
};
// 查找节点的辅助函数
// Helper function to find node
const findNodeInTree = (nodes: TreeNodeData[], key: string): TreeNodeData | null => {
for (const node of nodes) {
if (String(node.key) === key) {
@@ -393,7 +393,7 @@ const FolderTree: FC<FolderTreeProps> = ({
return null;
};
// 渐进式自动展开到指定路径
// Progressive auto expand to specified path
useEffect(() => {
if (!autoExpandPath || autoExpandPath.length === 0 || autoExpandInProgress || treeData.length === 0) {
return;
@@ -406,46 +406,46 @@ const FolderTree: FC<FolderTreeProps> = ({
const keysToExpand: React.Key[] = [];
let currentTreeData = treeData;
// 逐级展开,从第一级开始(跳过根节点,因为根节点已经加载)
// Expand level by level, starting from first level (skip root node as it's already loaded)
for (let i = 0; i < autoExpandPath.length - 1; i++) {
const nodeKey = autoExpandPath[i].id;
keysToExpand.push(nodeKey);
// 查找当前节点
// Find current node
const targetNode = findNodeInTree(currentTreeData, nodeKey);
if (targetNode && targetNode.children === undefined) {
// 如果子节点未加载,先加载
// If child nodes not loaded, load first
try {
console.log(`自动展开:加载节点 ${nodeKey} 的子节点`);
console.log(`Auto expand: Loading child nodes of ${nodeKey}`);
const children = await buildTreeNodes(knowledgeBaseId, nodeKey);
// 更新树数据
// Update tree data
setTreeData((prevData) => {
const newData = updateTreeData(prevData, nodeKey, children);
currentTreeData = newData; // 更新当前引用
currentTreeData = newData; // Update current reference
return newData;
});
// 等待状态更新完成
// Wait for state update to complete
await new Promise(resolve => setTimeout(resolve, 150));
} catch (error) {
console.error(`自动展开时加载节点 ${nodeKey} 失败:`, error);
// 加载失败时停止展开
console.error(`Failed to load node ${nodeKey} during auto expand:`, error);
// Stop expanding on load failure
break;
}
}
}
// 设置展开的节点
// Set expanded nodes
setExpandedKeys(keysToExpand);
// 选中最后一个节点(目标文件夹)
// Select last node (target folder)
const targetKey = autoExpandPath[autoExpandPath.length - 1]?.id;
if (targetKey) {
console.log(`自动展开:选中目标节点 ${targetKey}`);
// 延迟选中,确保展开动画完成
console.log(`Auto expand: Select target node ${targetKey}`);
// Delay selection to ensure expand animation completes
setTimeout(() => {
if (onSelect) {
onSelect([targetKey], {
@@ -460,21 +460,21 @@ const FolderTree: FC<FolderTreeProps> = ({
}
} catch (error) {
console.error('自动展开路径失败:', error);
console.error('Auto expand path failed:', error);
} finally {
// 延迟重置标志,确保展开过程完全完成
// Delay reset flag to ensure expand process is fully complete
setTimeout(() => {
setAutoExpandInProgress(false);
}, 500);
}
};
// 延迟执行,确保树数据已经加载完成
// Delay execution to ensure tree data is loaded
const timer = setTimeout(expandToPath, 300);
return () => clearTimeout(timer);
}, [autoExpandPath, treeData.length, knowledgeBaseId, onSelect, autoExpandInProgress]);
// 处理展开事件
// Handle expand event
const handleExpand: TreeProps['onExpand'] = (expandedKeys, info) => {
setExpandedKeys(expandedKeys);
if (onExpand) {
@@ -482,7 +482,7 @@ const FolderTree: FC<FolderTreeProps> = ({
}
};
// 处理选择事件,计算并传递路径
// Handle select event, calculate and pass path
const handleSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
if (selectedKeys.length > 0) {
const path = findNodePath(treeData, selectedKeys[0]);
@@ -493,7 +493,7 @@ const FolderTree: FC<FolderTreeProps> = ({
onFolderPathChange([]);
}
// 调用原始的 onSelect 回调
// Call original onSelect callback
if (onSelect) {
onSelect(selectedKeys, info);
}
@@ -503,7 +503,7 @@ const FolderTree: FC<FolderTreeProps> = ({
return (
<DirectoryTree
key={refreshKey} // 添加key确保refreshKey变化时重新渲染整个组件
key={refreshKey} // Add key to ensure component re-renders when refreshKey changes
multiple={multiple}
className={className}
style={style}

View File

@@ -130,7 +130,7 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
}
}
} catch (error) {
console.error('操作失败:', error);
console.error('Operation failed:', error);
const errorMsg = isEditMode
? (t('knowledgeBase.updateFailed') || '更新失败')
: (t('knowledgeBase.insertFailed') || '插入失败');

View File

@@ -9,7 +9,7 @@ import pointer from '@/assets/images/userMemory/pointer.svg'
import empty from '@/assets/images/userMemory/empty.svg'
import Empty from '@/components/Empty'
// 知识图谱数据类型定义
// Knowledge graph data type definitions
export interface KnowledgeNode {
id: string
entity_name: string
@@ -17,7 +17,7 @@ export interface KnowledgeNode {
description: string
pagerank: number
source_id: string[]
// ECharts 需要的属性
// Properties required by ECharts
name: string
category: number
symbolSize: number
@@ -35,7 +35,7 @@ export interface KnowledgeEdge {
source_id: string[]
source: string
target: string
// ECharts 需要的属性
// Properties required by ECharts
value: number
}
@@ -65,7 +65,7 @@ const operations = [
{ name: 'zoom', icon: zoom },
]
// 预定义的颜色调色板
// Predefined color palette
const colorPalette = [
'#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21',
'#FF5D34', '#FF8A4C', '#FFB048', '#E74C3C', '#9B59B6',
@@ -73,7 +73,7 @@ const colorPalette = [
'#8E44AD', '#2980B9', '#16A085', '#F1C40F', '#E67E22'
]
// 动态生成实体类型颜色映射
// Dynamically generate entity type color mapping
const generateEntityTypeColors = (entityTypes: string[]): Record<string, string> => {
const colorMap: Record<string, string> = {}
entityTypes.forEach((type, index) => {
@@ -93,12 +93,12 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
const [selectedNode, setSelectedNode] = useState<KnowledgeNode | null>(null)
const [entityTypeColors, setEntityTypeColors] = useState<Record<string, string>>({})
// 弹框拖动相关状态
// Modal drag-related state
const [modalPosition, setModalPosition] = useState({ x: 20, y: 20 })
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
// 拖动处理函数
// Drag handling functions
const handleMouseDown = useCallback((e: React.MouseEvent) => {
setIsDragging(true)
setDragStart({
@@ -113,7 +113,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
const newX = e.clientX - dragStart.x
const newY = e.clientY - dragStart.y
// 限制拖动范围,确保弹框不会超出容器
// Limit drag range to ensure modal doesn't exceed container bounds
const container = chartRef.current?.getEchartsInstance().getDom().parentElement
if (container && modalRef.current) {
const containerRect = container.getBoundingClientRect()
@@ -133,7 +133,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
setIsDragging(false)
}, [])
// 添加全局鼠标事件监听
// Add global mouse event listeners
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove)
@@ -145,12 +145,12 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
}
}, [isDragging, handleMouseMove, handleMouseUp])
// 关闭弹框
// Close modal
const handleCloseModal = useCallback(() => {
setSelectedNode(null)
}, [])
// 处理知识图谱数据
// Process knowledge graph data
const processGraphData = useCallback(() => {
if (!data?.graph) {
setNodes([])
@@ -164,31 +164,31 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
const processedNodes: KnowledgeNode[] = []
const processedEdges: KnowledgeEdge[] = []
// 获取所有实体类型
// Get all entity types
const entityTypes = [...new Set(rawNodes.map(node => node.entity_type))]
const categoryMap = entityTypes.reduce((acc, type, index) => {
acc[type] = index
return acc
}, {} as Record<string, number>)
// 动态生成实体类型颜色映射
// Dynamically generate entity type color mapping
const dynamicEntityTypeColors = generateEntityTypeColors(entityTypes)
setEntityTypeColors(dynamicEntityTypeColors)
// 计算每个节点的连接数
// Calculate connection count for each node
const connectionCount: Record<string, number> = {}
rawEdges.forEach(edge => {
// 使用 src_id tgt_id 计算连接数
// Use src_id and tgt_id to calculate connection count
connectionCount[edge.src_id] = (connectionCount[edge.src_id] || 0) + 1
connectionCount[edge.tgt_id] = (connectionCount[edge.tgt_id] || 0) + 1
})
// 处理节点数据
// Process node data
rawNodes.forEach(node => {
const connections = connectionCount[node.id] || 0
const categoryIndex = categoryMap[node.entity_type] || 0
// 根据 pagerank 和连接数计算节点大小
// Calculate node size based on pagerank and connection count
let symbolSize = Math.max(10, Math.min(50, node.pagerank * 200 + connections * 2))
processedNodes.push({
@@ -202,19 +202,19 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
})
})
// 处理边数据
// Process edge data
rawEdges.forEach(edge => {
// 注意:根据数据结构,source target 字段可能与 src_id tgt_id 相反
// 我们使用 src_id tgt_id 作为正确的连接关系
// Note: Based on data structure, source and target fields may be opposite to src_id and tgt_id
// We use src_id and tgt_id as the correct connection relationship
processedEdges.push({
...edge, // 保留所有原始字段
source: edge.src_id, // 使用 src_id 作为源节点
target: edge.tgt_id, // 使用 tgt_id 作为目标节点
...edge, // Keep all original fields
source: edge.src_id, // Use src_id as source node
target: edge.tgt_id, // Use tgt_id as target node
value: edge.weight || 1
})
})
// 验证节点ID和边的连接
// Verify node IDs and edge connections
const nodeIds = new Set(processedNodes.map(n => n.id))
const validEdges = processedEdges.filter(edge => {
const sourceExists = nodeIds.has(edge.source)
@@ -225,18 +225,18 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
return sourceExists && targetExists
})
// 调试信息
// Debug information
console.log('Total nodes:', processedNodes.length)
console.log('Total edges:', processedEdges.length)
console.log('Valid edges:', validEdges.length)
console.log('Node IDs:', Array.from(nodeIds).slice(0, 5))
console.log('Edge sample:', validEdges.slice(0, 3))
// 设置分类
// Set categories
const processedCategories = entityTypes.map(type => ({ name: type }))
setNodes(processedNodes)
setLinks(validEdges) // 只使用有效的边
setLinks(validEdges) // Only use valid edges
setCategories(processedCategories)
}, [data])
@@ -334,7 +334,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
lineStyle: {
color: '#5B6167',
curveness: 0.3,
width: 2, // 固定线宽,避免函数问题
width: 2, // Fixed line width to avoid function issues
opacity: 0.8
},
force: {
@@ -376,7 +376,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
}}
/>
{/* 实体详情弹框 */}
{/* Entity details modal */}
{selectedNode && (
<div
ref={modalRef}
@@ -387,7 +387,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
cursor: isDragging ? 'grabbing' : 'grab'
}}
>
{/* 弹框头部 - 可拖动区域 */}
{/* Modal header - draggable area */}
<div
className="rb:flex rb:items-center rb:justify-between rb:mb-3 rb:pb-2 rb:border-b rb:border-[#EBEBEB] rb:cursor-grab"
onMouseDown={handleMouseDown}
@@ -404,7 +404,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
</button>
</div>
{/* 弹框内容 */}
{/* Modal content */}
<div>
<div className="rb:font-medium rb:mb-4">
<div className="rb:text-[16px] rb:mb-2">{selectedNode.entity_name}</div>

View File

@@ -15,7 +15,7 @@ import { type KnowledgeBase } from '../types';
import Empty from '@/components/Empty';
interface KnowledgeGraphCardProps {
knowledgeBase?: KnowledgeBase;
onRebuildGraph?: () => void; // 添加重建图谱的回调函数
onRebuildGraph?: () => void; // Callback function to rebuild graph
}
const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBase, onRebuildGraph }) => {
@@ -23,7 +23,7 @@ const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBase,
const [data, setData] = useState<KnowledgeGraphResponse | undefined>()
const [loading, setLoading] = useState(true)
const handleRebuildGraph = () => {
// 调用父组件传递的回调函数来打开CreateModal并传递重建标识
// Call parent component's callback to open CreateModal with rebuild flag
if (onRebuildGraph) {
onRebuildGraph();
}
@@ -38,15 +38,15 @@ const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBase,
setLoading(true)
try {
const res = await getKnowledgeGraph(knowledgeBase?.id)
// 判断 res.graph 是否为空对象或不存在
// Check if res.graph is empty object or doesn't exist
const graphResponse = res as KnowledgeGraphResponse;
if (!graphResponse || !graphResponse.graph || Object.keys(graphResponse.graph).length === 0) {
setData(undefined) // 设置为 undefined 以显示 empty 状态
setData(undefined) // Set to undefined to show empty state
} else {
setData(graphResponse)
}
} catch (error) {
console.error('获取知识图谱数据失败:', error)
console.error('Failed to fetch knowledge graph data:', error)
} finally {
setLoading(false)
}

View File

@@ -27,7 +27,7 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
{ label: t('knowledgeBase.vector'), value: false },
]);
// 获取检索模式选项
// Get retrieval mode options
useEffect(() => {
fetchRetrievalModeOptions();
}, []);
@@ -36,9 +36,9 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
try {
const response = await getRetrievalModeType();
if (response && Array.isArray(response)) {
// 将 API 返回的数据转换为选项格式
// Convert API response to option format
const options = response.map((item: any) => {
// 支持多种数据格式
// Support multiple data formats
let label = t(`knowledgeBase.${item}`) + ' ' + t(`knowledgeBase.retrieve`);
let value = item;
@@ -50,8 +50,8 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
}
}
} catch (error) {
console.error('获取检索模式选项失败:', error);
// 保持默认选项
console.error('Failed to fetch retrieval mode options:', error);
// Keep default options
}
};
@@ -60,8 +60,8 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
setKnowledgeBaseId(kbId || '');
form.resetFields();
setData([]);
setRetrieveType('hybrid'); // 重置为默认值
// 确保表单字段也设置为默认值
setRetrieveType('hybrid'); // Reset to default value
// Ensure form field is also set to default value
form.setFieldsValue({ retrieve_type: 'hybrid' });
}
const fetchData = (params: RecallTestParams) => {
@@ -91,10 +91,10 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
console.log('RecallTest - params:', params);
fetchData(params);
}).catch((error) => {
console.error('表单验证失败:', error);
console.error('Form validation failed:', error);
});
}
// 暴露给父组件的方法
// Expose methods to parent component
useImperativeHandle(ref, () => ({
handleOpen,
}));
@@ -134,7 +134,7 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
/>
</Form.Item>
{/* retrieve_type = semantic hybrid 时显示 */}
{/* Show when retrieve_type = semantic or hybrid */}
{(retrieveType === 'semantic' || retrieveType === 'hybrid') && (
<Form.Item name="similarity_threshold" label={t('knowledgeBase.similarityThreshold')}>
<Select
@@ -155,7 +155,7 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
</Form.Item>
)}
{/* retrieve_type = participle hybrid 时显示 */}
{/* Show when retrieve_type = participle or hybrid */}
{(retrieveType === 'participle' || retrieveType === 'hybrid') && (
<Form.Item name="vector_similarity_weight" label={t('knowledgeBase.semanticSimilarity')}>
<Select

View File

@@ -12,7 +12,7 @@ const RecallTestDrawer = forwardRef<RecallTestDrawerRef>(({},ref) => {
const pendingKbIdRef = useRef<string | undefined>(undefined);
const shouldCallHandleOpenRef = useRef(false);
// 调用 RecallTest handleOpen 方法
// Call RecallTest's handleOpen method
const callRecallTestHandleOpen = useCallback(() => {
if (recallTestRef.current && shouldCallHandleOpenRef.current) {
recallTestRef.current.handleOpen(pendingKbIdRef.current);
@@ -26,14 +26,14 @@ const RecallTestDrawer = forwardRef<RecallTestDrawerRef>(({},ref) => {
setOpen(true);
}
// Drawer 打开时,尝试调用 handleOpen
// When Drawer opens, try to call handleOpen
useLayoutEffect(() => {
if (open) {
callRecallTestHandleOpen();
}
}, [open, callRecallTestHandleOpen]);
// 使用回调 ref 确保在组件挂载后立即调用
// Use callback ref to ensure immediate call after component mount
const setRecallTestRef = useCallback((node: any) => {
recallTestRef.current = node;
if (open && shouldCallHandleOpenRef.current) {
@@ -41,7 +41,7 @@ const RecallTestDrawer = forwardRef<RecallTestDrawerRef>(({},ref) => {
}
}, [open, callRecallTestHandleOpen]);
// 暴露给父组件的方法
// Expose methods to parent component
useImperativeHandle(ref, () => ({
handleOpen,
}));

View File

@@ -1,5 +1,5 @@
/*
* @Description: 滚动列表
/**
* @Description: Scroll List
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2025-11-18 16:19:58
@@ -22,9 +22,9 @@ interface RecallTestResultProps {
loadMore?: () => void;
loading?: boolean;
scrollableTarget?: string;
editable?: boolean; // 是否可编辑
onItemClick?: (item: RecallTestData, index: number) => void; // 点击项的回调
parserMode?: number; // 解析模式1 表示 QA 格式
editable?: boolean; // Whether editable
onItemClick?: (item: RecallTestData, index: number) => void; // Click item callback
parserMode?: number; // Parser mode, 1 means QA format
}
const RecallTestResult = ({
@@ -40,7 +40,7 @@ const RecallTestResult = ({
}: RecallTestResultProps) => {
const { t } = useTranslation();
// 解析 QA 格式内容
// Parse QA format content
const parseQAContent = (content: string) => {
if (!content || parserMode !== 1) return null;
@@ -56,25 +56,25 @@ const RecallTestResult = ({
return null;
};
// 格式化 QA 内容为显示格式
// Format QA content for display
const formatQAContent = (question: string, answer: string) => {
return `**${t('knowledgeBase.question')}:** ${question}\n**${t('knowledgeBase.answer')}:** ${answer}`;
};
const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => {
// 检查点击的是否是图片或图片相关元素
// Check if the click is on an image or image-related element
const target = e.target as HTMLElement;
// 检查是否点击了图片本身、图片的容器、预览层、关闭按钮或 SVG 图标
// Check if clicked on image itself, image container, preview layer, close button or SVG icon
if (
target.tagName === 'IMG' ||
target.tagName === 'SVG' || // SVG 图标
target.tagName === 'PATH' || // SVG 路径
target.tagName === 'SVG' || // SVG icon
target.tagName === 'PATH' || // SVG path
target.closest('.ant-image') ||
target.closest('.ant-image-preview') ||
target.closest('.ant-image-preview-wrap') ||
target.closest('.ant-image-preview-operations') ||
target.closest('.anticon') || // Ant Design 图标
target.closest('.anticon') || // Ant Design icon
target.classList.contains('ant-image-img') ||
target.classList.contains('ant-image-mask') ||
target.classList.contains('ant-image-preview-close') ||
@@ -88,7 +88,7 @@ const RecallTestResult = ({
}
};
// 根据分数获取颜色类名
// Get color class based on score
const getScoreColorClass = (score: number): string => {
const percentage = score * 100;
if (percentage >= 90) {
@@ -177,7 +177,7 @@ const RecallTestResult = ({
</div>
);
// 如果提供了 loadMore hasMore,使用 InfiniteScroll
// If loadMore and hasMore are provided, use InfiniteScroll
if (loadMore && hasMore !== undefined) {
return (
<div className='rb:flex rb:h-full rb:flex-col'>
@@ -200,7 +200,7 @@ const RecallTestResult = ({
);
}
// 否则使用普通渲染
// Otherwise use normal rendering
return (
<div className='rb:flex rb:flex-col'>
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>

View File

@@ -4,13 +4,13 @@
* @Author: yujiangping
* @Date: 2025-11-10 18:52:55
* @LastEditors: yujiangping
* @LastEditTime: 2025-11-29 12:29:31
* @LastEditTime: 2026-02-03 17:08:00
*/
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
import { Switch } from 'antd';
import { useTranslation } from 'react-i18next';
import { message } from 'antd';
import type { ShareModalRef, ShareModalRefProps, KnowledgeBase} from '@/views/KnowledgeBase/types';
import type { ShareModalRef, ShareModalRefProps, KnowledgeBase, SpaceItem} from '@/views/KnowledgeBase/types';
import RbModal from '@/components/RbModal'
// import betchControlIcon from '@/assets/images/knowledgeBase/betch-control.png';
import kbIcon from '@/assets/images/knowledgeBase/knowledge-management.png';
@@ -33,7 +33,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBase | null>(null);
const [spaceList, setSpaceList] = useState<SpaceItem[]>([]);
// 封装取消方法,添加关闭弹窗逻辑
// Close modal and reset state
const handleClose = () => {
setCurIndex(9999);
setLoading(false)
@@ -66,11 +66,11 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
console.log('Workspace IDs:', workspaceIds);
shareSpaceModalRef?.current?.handleOpen(kbId,knowledgeBase,workspaceIds);
// 分享后关闭弹窗
// Close modal after sharing
handleClose();
}
const handleChange = (checked: boolean, item: any) => {
// 打开/关闭分享出去的数据库
// Toggle shared knowledge base status
console.log('Switch changed:', checked, item);
updateKnowledgeBase(item.target_kb?.id, {
status: checked ? 1 : 2
@@ -82,7 +82,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
})
}
// 暴露给父组件的方法
// Expose methods to parent component
useImperativeHandle(ref, () => ({
handleOpen,
handleClose,

View File

@@ -30,7 +30,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBase | null>(null);
const [spaceList, setSpaceList] = useState<SpaceItem[]>([]);
// 封装取消方法,添加关闭弹窗逻辑
// Close modal and reset state
const handleClose = () => {
setCurIndex(-1);
setLoading(false)
@@ -51,10 +51,10 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
}
const handleShare = async() => {
// 获取所有 checked true 的数据
// Get all data with checked = true
const checkedItems = spaceList.filter(item => item.is_active);
debugger
// 获取当前选中的项(curIndex 对应的数据)
// Get currently selected item (corresponding to curIndex)
const selectedItem = curIndex !== -1 ? spaceList[curIndex] : null;
if(!selectedItem){
messageApi.error(t('knowledgeBase.selectSpace'));
@@ -70,13 +70,13 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
}else{
messageApi.error(t('knowledgeBase.shareFailed'));
}
// 调用父组件传递的回调函数,传递选中的数据
// Call parent component's callback function with selected data
onShare?.({
checkedItems,
selectedItem
});
// 分享后关闭弹窗
// Close modal after sharing
handleClose();
}
const handleClick = (index: number, checked: boolean) => {
@@ -84,7 +84,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
setCurIndex(index);
}
// 暴露给父组件的方法
// Expose methods to parent component
useImperativeHandle(ref, () => ({
handleOpen,
handleClose,

View File

@@ -36,11 +36,11 @@ const Datasets: FC = () => {
};
if (loading) {
return <div>...</div>;
return <div>Loading...</div>;
}
if (!knowledgeBase) {
return <div></div>;
return <div>Knowledge base not found</div>;
}
return (
@@ -50,7 +50,7 @@ const Datasets: FC = () => {
icon={<ArrowLeftOutlined />}
onClick={handleBack}
>
{t('common.back')}
</Button>
</div>
@@ -61,7 +61,7 @@ const Datasets: FC = () => {
<div className="rb:bg-white rb:p-4 rb:rounded">
<h2 className="rb:text-lg rb:font-semibold rb:mb-4">{t('knowledgeBase.datasets')}</h2>
{/* TODO: 添加数据集列表 */}
{/* TODO: Add dataset list */}
<div>{t('knowledgeBase.noDataSets')}</div>
</div>
</div>

View File

@@ -388,7 +388,7 @@ const KnowledgeBaseManagement: FC = () => {
});
},
onCancel: () => {
console.log('取消删除');
console.log('Cancel delete');
},
});
};

View File

@@ -51,7 +51,6 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
const handleDelete = (vo: any) => {
modal.confirm({
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
content: t('application.apiKeyDeleteContent'),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:42
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Transfer, type TransferProps, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
@@ -12,23 +18,37 @@ import Tag from '@/components/Tag';
const FormItem = Form.Item;
/**
* Props for OntologyClassExtractModal component
*/
interface OntologyClassExtractModalProps {
/** Callback function to refresh parent list after extraction */
refresh: () => void;
}
/**
* Modal component for extracting ontology classes using LLM
* Two-step process: 1) Extract classes from scenario 2) Select and confirm classes to add
*/
const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, OntologyClassExtractModalProps>(({
refresh
}, ref) => {
// Hooks
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<OntologyClassExtractModalData>();
// State
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false)
const [data, setData] = useState<OntologyClassData | null>(null)
const [extractData, setExtractData] = useState<ExtractData | null>(null)
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
const [selectedKeys, setSelectedKeys] = useState<TransferProps['selectedKeys']>([]);
/**
* Close modal and reset all state
*/
const handleClose = () => {
setVisible(false);
form.resetFields();
@@ -37,11 +57,19 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
setExtractData(null)
};
/**
* Open modal with scene data
* @param vo - Ontology class data containing scene information
*/
const handleOpen = (vo: OntologyClassData) => {
form.resetFields();
setVisible(true);
setData(vo)
};
/**
* Execute LLM extraction to get class suggestions
*/
const handleSave = () => {
if (!data?.scene_id) return;
form
@@ -67,6 +95,10 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
});
}
/**
* Confirm and create selected classes
* First click runs extraction, second click creates classes
*/
const handleConfirm = () => {
if (!extractData) {
handleSave()
@@ -90,11 +122,19 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
}
}
/**
* Handle transfer component target keys change
* @param nextTargetKeys - New target keys after transfer
*/
const onChange: TransferProps['onChange'] = (nextTargetKeys) => {
setTargetKeys(nextTargetKeys.filter(Boolean));
};
/**
* Handle transfer component selection change
* @param sourceSelectedKeys - Selected keys in source list
* @param targetSelectedKeys - Selected keys in target list
*/
const onSelectChange: TransferProps['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
@@ -102,6 +142,9 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys].filter(Boolean));
};
/**
* Expose methods to parent component via ref
*/
useImperativeHandle(ref, () => ({
handleOpen,
}));

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:39
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:39
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
@@ -8,31 +14,53 @@ import { createOntologyClass } from '@/api/ontology'
const FormItem = Form.Item;
/**
* Props for OntologyClassModal component
*/
interface OntologyClassModalProps {
/** Callback function to refresh parent list after save */
refresh: () => void;
}
/**
* Modal component for adding new ontology classes
* Provides form interface for class name and description
*/
const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalProps>(({
refresh
}, ref) => {
// Hooks
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<AddClassItem>();
// State
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false)
const [scene_id, setSceneId] = useState<string | null>(null)
/**
* Close modal and reset form state
*/
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
/**
* Open modal for adding a new class
* @param scene_id - Target scene identifier
*/
const handleOpen = (scene_id: string) => {
form.resetFields();
setVisible(true);
setSceneId(scene_id)
};
/**
* Validate and submit form data to create new class
*/
const handleSave = () => {
if (!scene_id) return;
form
@@ -54,6 +82,9 @@ const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalP
});
}
/**
* Expose methods to parent component via ref
*/
useImperativeHandle(ref, () => ({
handleOpen,
}));

View File

@@ -0,0 +1,144 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:46
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:46
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, App, Select, type SelectProps } from 'antd';
import { useTranslation } from 'react-i18next';
import type { OntologyExportModalData, OntologyExportModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { ontologyExport, getOntologyScenesUrl } from '@/api/ontology'
import CustomSelect from '@/components/CustomSelect';
const FormItem = Form.Item;
/**
* Props for OntologyExportModal component
*/
interface OntologyExportModalProps {
/** Callback function to refresh parent list after export */
refresh: () => void;
}
/**
* Modal component for exporting ontology scenes
* Supports RDF/XML (.owl) and Turtle (.ttl) formats
*/
const OntologyExportModal = forwardRef<OntologyExportModalRef, OntologyExportModalProps>(({
refresh
}, ref) => {
// Hooks
const { t } = useTranslation();
const { message } = App.useApp();
const [form] = Form.useForm<OntologyExportModalData>();
// State
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false)
const [fileName, setFileName] = useState('')
/**
* Close modal and reset form state
*/
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
/**
* Open the export modal
*/
const handleOpen = () => {
form.resetFields();
setVisible(true);
};
/**
* Handle scene selection change to set export filename
* @param _value - Selected scene ID
* @param option - Selected option containing scene name
*/
const handleChange: SelectProps['onChange'] = (_value, option) => {
const name = Array.isArray(option) ? option[0]?.children : option?.children;
setFileName(String(name || ''));
}
/**
* Validate and submit form data to export ontology
* Downloads file with appropriate extension based on format
*/
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
ontologyExport(values, `${fileName}.${values.format === 'rdfxml' ?'owl' : 'ttl'}`, () => {
message.success(t('common.exportSuccess'));
handleClose();
refresh();
setLoading(false)
})
})
.catch((err) => {
console.log('err', err)
});
}
/**
* Expose methods to parent component via ref
*/
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={t('ontology.export')}
open={visible}
onCancel={handleClose}
okText={t('common.export')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
initialValues={{ format: 'rdfxml' }}
>
<FormItem
name="scene_id"
label={t('ontology.scene_id')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<CustomSelect
url={getOntologyScenesUrl}
params={{ page: 1, pagesize: 100 }}
valueKey="scene_id"
labelKey="scene_name"
hasAll={false}
onChange={handleChange}
/>
</FormItem>
<FormItem
name="format"
label={t('ontology.format')}
>
<Select
placeholder={t('common.pleaseSelect')}
options={[
{ value: 'rdfxml', label: 'RDF/XML' },
{ value: 'turtle', label: 'Turtle' },
]}
/>
</FormItem>
</Form>
</RbModal>
);
});
export default OntologyExportModal;

View File

@@ -0,0 +1,139 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:32
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:32
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { OntologyImportModalData, OntologyImportModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { ontologyImport } from '@/api/ontology'
import UploadFiles from '@/components/Upload/UploadFiles';
const FormItem = Form.Item;
/**
* Props for OntologyImportModal component
*/
interface OntologyImportModalProps {
/** Callback function to refresh parent list after import */
refresh: () => void;
}
/**
* Modal component for importing ontology files
* Supports OWL, TTL, RDF, XML file formats
*/
const OntologyImportModal = forwardRef<OntologyImportModalRef, OntologyImportModalProps>(({
refresh
}, ref) => {
// Hooks
const { t } = useTranslation();
const { message } = App.useApp();
const [form] = Form.useForm<OntologyImportModalData>();
// State
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false)
/**
* Close modal and reset form state
*/
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
/**
* Open the import modal
*/
const handleOpen = () => {
form.resetFields();
setVisible(true);
};
/**
* Validate and submit form data to import ontology file
* Creates FormData with file and scene information
*/
const handleSave = () => {
form
.validateFields()
.then((values) => {
const { scene_name, scene_description, file } = values
console.log('values', file);
const formData = new FormData();
formData.append('file', file[0]);
formData.append('scene_name', scene_name);
if (scene_description) {
formData.append('scene_description', scene_description);
}
setLoading(true)
ontologyImport(formData)
.then(() => {
message.success(t('common.saveSuccess'));
handleClose();
refresh();
})
.finally(() => setLoading(false))
})
.catch((err) => {
console.log('err', err)
});
}
/**
* Expose methods to parent component via ref
*/
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={t('ontology.import')}
open={visible}
onCancel={handleClose}
okText={t('common.create')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<FormItem
name="scene_name"
label={t('ontology.scene_name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
<FormItem
name="scene_description"
label={t('ontology.scene_description')}
>
<Input.TextArea placeholder={t('common.enter')} />
</FormItem>
<FormItem
name="file"
label={t('ontology.file')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<UploadFiles
isCanDrag={true}
fileType={['owl', 'ttl', 'rdf', 'xml']}
isAutoUpload={false}
/>
</FormItem>
</Form>
</RbModal>
);
});
export default OntologyImportModal;

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:28
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:28
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
@@ -8,20 +14,34 @@ import { createOntologyScene, updateOntologyScene } from '@/api/ontology'
const FormItem = Form.Item;
/**
* Props for OntologyModal component
*/
interface OntologyModalProps {
/** Callback function to refresh parent list after save */
refresh: () => void;
}
/**
* Modal component for creating or editing ontology scenes
* Provides form interface for scene name and description
*/
const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
refresh
}, ref) => {
// Hooks
const { t } = useTranslation();
const { message } = App.useApp();
const [form] = Form.useForm<OntologyModalData>();
// State
const [visible, setVisible] = useState(false);
const [editVo, setEditVo] = useState<OntologyItem | null>(null)
const [form] = Form.useForm<OntologyModalData>();
const [loading, setLoading] = useState(false)
/**
* Close modal and reset form state
*/
const handleClose = () => {
setVisible(false);
form.resetFields();
@@ -29,6 +49,10 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
setEditVo(null)
};
/**
* Open modal for creating or editing
* @param vo - Optional ontology item data for edit mode
*/
const handleOpen = (vo?: OntologyItem) => {
if (vo) {
setEditVo(vo);
@@ -38,6 +62,11 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
}
setVisible(true);
};
/**
* Validate and submit form data
* Creates new scene or updates existing one based on editVo
*/
const handleSave = () => {
form
.validateFields()
@@ -57,6 +86,9 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
});
}
/**
* Expose methods to parent component via ref
*/
useImperativeHandle(ref, () => ({
handleOpen,
}));

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:24
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:56
*/
import { type FC, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { Layout, Button } from 'antd';
@@ -6,11 +12,23 @@ import logoutIcon from '@/assets/images/logout_hover.svg'
const { Header } = Layout;
/**
* Props for PageHeader component
*/
interface ConfigHeaderProps {
/** Page title/name */
name?: string;
/** Subtitle content displayed below the title */
subTitle?: ReactNode | string;
/** Extra content displayed on the right side */
extra?: ReactNode;
}
/**
* Page header component for ontology pages
* Displays title, subtitle, back button and extra actions
* @param props - Component props
*/
const PageHeader: FC<ConfigHeaderProps> = ({
name,
subTitle,
@@ -19,6 +37,9 @@ const PageHeader: FC<ConfigHeaderProps> = ({
const { t } = useTranslation();
const navigate = useNavigate();
/**
* Navigate back to previous page
*/
const goBack = () => {
navigate(-1)
}

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:15
*/
import { type FC, useState, useRef, type MouseEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -5,29 +11,57 @@ import { Row, Col, Button, Flex, Divider, Space, App, Tooltip } from 'antd'
import SearchInput from '@/components/SearchInput';
import OntologyModal from './components/OntologyModal'
import type { OntologyModalRef, OntologyItem, Query } from './types'
import type { OntologyModalRef, OntologyItem, Query, OntologyImportModalRef, OntologyExportModalRef } from './types'
import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { getOntologyScenesUrl, deleteOntologyScene } from '@/api/ontology'
import { formatDateTime } from '@/utils/format'
import OntologyImportModal from './components/OntologyImportModal'
import OntologyExportModal from './components/OntologyExportModal'
/**
* Ontology management page component
* Displays a list of ontology scenes with search, create, import, export functionality
*/
const Ontology: FC = () => {
// Hooks
const { t } = useTranslation();
const navigate = useNavigate()
const { modal, message } = App.useApp();
// State
const [query, setQuery] = useState<Query>({});
// Refs
const scrollListRef = useRef<PageScrollListRef>(null)
const entityModalRef = useRef<OntologyModalRef>(null)
const ontologyImportModalRef = useRef<OntologyImportModalRef>(null)
const ontologyExportModalRef = useRef<OntologyExportModalRef>(null)
/**
* Open modal to create a new ontology scene
*/
const handleCreate = () => {
entityModalRef.current?.handleOpen()
}
/**
* Open modal to edit an existing ontology scene
* @param record - The ontology item to edit
* @param e - Mouse event to prevent propagation
*/
const handleEdit = (record: OntologyItem, e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
entityModalRef.current?.handleOpen(record)
}
/**
* Delete an ontology scene with confirmation
* @param item - The ontology item to delete
* @param e - Mouse event to prevent propagation
*/
const handleDelete = (item: OntologyItem, e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -45,9 +79,35 @@ const Ontology: FC = () => {
}
})
}
/**
* Navigate to ontology detail page
* @param record - The ontology item to view
*/
const handleJump = (record: OntologyItem) => {
navigate(`/ontology/${record.scene_id}`)
}
/**
* Refresh the ontology list
*/
const handleRefresh = () => {
scrollListRef.current?.refresh()
}
/**
* Open export modal
*/
const handleExport = () => {
ontologyExportModalRef.current?.handleOpen()
}
/**
* Open import modal
*/
const handleImport = () => {
ontologyImportModalRef.current?.handleOpen()
}
return (
<>
@@ -60,9 +120,17 @@ const Ontology: FC = () => {
/>
</Col>
<Col span={16} className="rb:text-right">
<Button type="primary" onClick={handleCreate}>
+ {t('ontology.create')}
</Button>
<Space size={12}>
<Button onClick={handleExport}>
{t('ontology.export')}
</Button>
<Button onClick={handleImport}>
{t('ontology.import')}
</Button>
<Button type="primary" onClick={handleCreate}>
+ {t('ontology.create')}
</Button>
</Space>
</Col>
</Row>
@@ -124,7 +192,15 @@ const Ontology: FC = () => {
<OntologyModal
ref={entityModalRef}
refresh={() => scrollListRef.current?.refresh()}
refresh={handleRefresh}
/>
<OntologyImportModal
ref={ontologyImportModalRef}
refresh={handleRefresh}
/>
<OntologyExportModal
ref={ontologyExportModalRef}
refresh={handleRefresh}
/>
</>
)

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:20
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:20
*/
import { type FC, useEffect, useState, useRef } from 'react'
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -12,22 +18,35 @@ import SearchInput from '@/components/SearchInput';
import OntologyClassExtractModal from '../components/OntologyClassExtractModal'
import BodyWrapper from '@/components/Empty/BodyWrapper'
/**
* Ontology detail page component
* Displays and manages classes within a specific ontology scene
*/
const Detail: FC = () => {
// Hooks
const { t } = useTranslation();
const { id } = useParams()
const { modal, message } = App.useApp()
// Refs
const ontologyClassModalRef = useRef<OntologyClassModalRef>(null)
const ontologyClassExtractModalRef = useRef<OntologyClassExtractModalRef>(null)
// State
const [query, setQuery] = useState<{
class_name?: string;
}>({});
const [loading, setLoading] = useState(false)
const [data, setData] = useState<OntologyClassData>({} as OntologyClassData)
// Fetch data when component mounts or dependencies change
useEffect(() => {
getData()
}, [id, query])
/**
* Fetch ontology class list data
*/
const getData = () => {
if (!id) return;
setLoading(true)
@@ -42,6 +61,11 @@ const Detail: FC = () => {
setLoading(false)
})
}
/**
* Delete an ontology class with confirmation
* @param item - The class item to delete
*/
const handleDelete = (item: OntologyClassItem) => {
modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.class_name }),
@@ -57,9 +81,17 @@ const Detail: FC = () => {
}
})
}
/**
* Open modal to add a new class
*/
const handleAdd = () => {
ontologyClassModalRef.current?.handleOpen(data.scene_id)
}
/**
* Open modal to extract classes using LLM
*/
const handleExtract = () => {
ontologyClassExtractModalRef.current?.handleOpen(data)
}

View File

@@ -1,79 +1,214 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:10
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 14:10:10
*/
/**
* Query parameters for ontology list pagination and filtering
*/
export interface Query {
/** Number of items per page */
pagesize?: number;
/** Current page number */
page?: number;
/** Scene name for filtering */
scene_name?: string;
}
/**
* Ontology scene item data structure
*/
export interface OntologyItem {
/** Unique identifier for the scene */
scene_id: string;
/** Name of the ontology scene */
scene_name: string;
/** Description of the ontology scene */
scene_description: string;
/** Number of entity types in the scene */
type_num: number;
/** Array of entity type names */
entity_type: string[];
/** Associated workspace identifier */
workspace_id: string;
/** Creation timestamp */
created_at: number;
/** Last update timestamp */
updated_at: number;
/** Total count of classes in the scene */
classes_count: number;
}
/**
* Form data for creating/editing ontology scene
*/
export interface OntologyModalData {
/** Scene name */
scene_name: string;
/** Scene description */
scene_description: string;
}
/**
* Ref methods exposed by OntologyModal component
*/
export interface OntologyModalRef {
/**
* Open the modal for creating or editing
* @param data - Optional ontology item data for editing mode
*/
handleOpen: (data?: OntologyItem) => void;
}
/**
* Ontology class item data structure
*/
export interface OntologyClassItem {
/** Unique identifier for the class */
class_id: string;
/** Name of the class */
class_name: string;
/** Description of the class */
class_description: string;
/** Associated scene identifier */
scene_id: string;
/** Creation timestamp */
created_at: number;
/** Last update timestamp */
updated_at: number;
}
/**
* Response data structure for ontology class list
*/
export interface OntologyClassData {
/** Total number of classes */
total: number;
/** Scene identifier */
scene_id: string;
/** Scene name */
scene_name: string;
/** Scene description */
scene_description: string;
/** Array of class items */
items: OntologyClassItem[];
}
/**
* Data structure for adding a new class
*/
export interface AddClassItem {
/** Name of the class to add */
class_name: string;
/** Description of the class to add */
class_description: string;
}
/**
* Form data for creating ontology classes
*/
export interface OntologyClassModalData {
/** Target scene identifier */
scene_id: string;
/** Array of classes to create */
classes: AddClassItem[]
}
/**
* Ref methods exposed by OntologyClassModal component
*/
export interface OntologyClassModalRef {
/**
* Open the modal for adding classes
* @param scene_id - Target scene identifier
*/
handleOpen: (scene_id: string) => void;
}
/**
* Form data for extracting ontology classes using LLM
*/
export interface OntologyClassExtractModalData {
/** LLM model identifier */
llm_id: string;
/** Target scene identifier */
scene_id: string;
/** Scenario description for extraction */
scenario: string;
domain: string; // scene_name
/** Domain name (same as scene_name) */
domain: string;
}
/**
* Ref methods exposed by OntologyClassExtractModal component
*/
export interface OntologyClassExtractModalRef {
/**
* Open the modal for extracting classes
* @param vo - Ontology class data containing scene information
*/
handleOpen: (vo: OntologyClassData) => void;
}
/**
* Extracted class item from LLM
*/
export interface ExtractClassItem {
/** Unique identifier for the extracted class */
id: string;
/** English name of the class */
name: string;
/** Chinese name of the class */
name_chinese: string;
/** Description of the class */
description: string;
/** Example instances of the class */
examples: string[];
/** Parent class name if exists */
parent_class: string | null;
/** Entity type classification */
entity_type: string;
/** Domain the class belongs to */
domain: string;
}
/**
* Response data structure for class extraction
*/
export interface ExtractData {
/** Domain name */
domain: string;
/** Number of classes extracted */
extracted_count: number;
/** Array of extracted class items */
classes: ExtractClassItem[]
}
/**
* Ref methods exposed by OntologyImportModal component
*/
export interface OntologyImportModalRef {
/** Open the import modal */
handleOpen: () => void;
}
/**
* Form data for importing ontology
*/
export interface OntologyImportModalData {
/** Name for the imported scene */
scene_name: string;
/** Optional description for the imported scene */
scene_description?: string;
/** File to import (OWL, TTL, RDF, XML formats) */
file: any;
}
/**
* Ref methods exposed by OntologyExportModal component
*/
export interface OntologyExportModalRef {
/** Open the export modal */
handleOpen: () => void;
}
/**
* Form data for exporting ontology
*/
export interface OntologyExportModalData {
/** Scene identifier to export */
scene_id: string;
/** Export format: 'rdfxml' (.owl) or 'turtle' (.ttl) */
format: 'rdfxml' | 'turtle';
}

View File

@@ -38,7 +38,6 @@ const History: React.FC<{ query: HistoryQuery; edit: (item: HistoryItem) => void
e?.stopPropagation();
modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.title }),
content: t('application.apiKeyDeleteContent'),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',

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