Merge branch 'develop' into fix/workflow

This commit is contained in:
Eternity
2026-01-14 10:58:13 +08:00
committed by GitHub
77 changed files with 3193 additions and 1889 deletions

View File

@@ -167,7 +167,6 @@ class Settings:
# official environment system version
SYSTEM_VERSION: str = os.getenv("SYSTEM_VERSION", "v0.2.0")
SYSTEM_INTRODUCTION: str = os.getenv("SYSTEM_INTRODUCTION", "")
def get_memory_output_path(self, filename: str = "") -> str:
"""

View File

@@ -1,6 +1,7 @@
import asyncio
import json
from datetime import datetime
from typing import List, Optional
from typing import List, Optional, Tuple
from uuid import uuid4
from app.core.logging_config import get_memory_logger
@@ -28,6 +29,118 @@ class MemorySummaryResponse(RobustLLMResponse):
)
async def generate_title_and_type_for_summary(
content: str,
llm_client
) -> Tuple[str, str]:
"""
为MemorySummary生成标题和类型
此方法应该在创建MemorySummary节点时调用生成title和type
Args:
content: Summary的内容文本
llm_client: LLM客户端实例
Returns:
(标题, 类型)元组
"""
from app.core.memory.utils.prompt.prompt_utils import render_episodic_title_and_type_prompt
# 定义有效的类型集合
VALID_TYPES = {
"conversation", # 对话
"project_work", # 项目/工作
"learning", # 学习
"decision", # 决策
"important_event" # 重要事件
}
DEFAULT_TYPE = "conversation" # 默认类型
try:
if not content:
logger.warning("content为空无法生成标题和类型")
return ("空内容", DEFAULT_TYPE)
# 1. 渲染Jinja2提示词模板
prompt = await render_episodic_title_and_type_prompt(content)
# 2. 调用LLM生成标题和类型
messages = [
{"role": "user", "content": prompt}
]
response = await llm_client.chat(messages=messages)
# 3. 解析LLM响应
content_response = response.content
if isinstance(content_response, list):
if len(content_response) > 0:
if isinstance(content_response[0], dict):
text = content_response[0].get('text', content_response[0].get('content', str(content_response[0])))
full_response = str(text)
else:
full_response = str(content_response[0])
else:
full_response = ""
elif isinstance(content_response, dict):
full_response = str(content_response.get('text', content_response.get('content', str(content_response))))
else:
full_response = str(content_response) if content_response is not None else ""
# 4. 解析JSON响应
try:
# 尝试从响应中提取JSON
# 移除可能的markdown代码块标记
json_str = full_response.strip()
if json_str.startswith("```json"):
json_str = json_str[7:]
if json_str.startswith("```"):
json_str = json_str[3:]
if json_str.endswith("```"):
json_str = json_str[:-3]
json_str = json_str.strip()
result_data = json.loads(json_str)
title = result_data.get("title", "未知标题")
episodic_type_raw = result_data.get("type", DEFAULT_TYPE)
# 5. 校验和归一化类型
# 将类型转换为小写并去除空格
episodic_type_normalized = str(episodic_type_raw).lower().strip()
# 检查是否在有效类型集合中
if episodic_type_normalized in VALID_TYPES:
episodic_type = episodic_type_normalized
else:
# 尝试映射常见的中文类型到英文
type_mapping = {
"对话": "conversation",
"项目": "project_work",
"工作": "project_work",
"项目/工作": "project_work",
"学习": "learning",
"决策": "decision",
"重要事件": "important_event",
"事件": "important_event"
}
episodic_type = type_mapping.get(episodic_type_raw, DEFAULT_TYPE)
logger.warning(
f"LLM返回的类型 '{episodic_type_raw}' 不在有效集合中,"
f"已归一化为 '{episodic_type}'"
)
logger.info(f"成功生成标题和类型: title={title}, type={episodic_type}")
return (title, episodic_type)
except json.JSONDecodeError:
logger.error(f"无法解析LLM响应为JSON: {full_response}")
return ("解析失败", DEFAULT_TYPE)
except Exception as e:
logger.error(f"生成标题和类型时出错: {str(e)}", exc_info=True)
return ("错误", DEFAULT_TYPE)
async def _process_chunk_summary(
dialog: DialogData,
chunk,
@@ -63,10 +176,9 @@ async def _process_chunk_summary(
title = None
episodic_type = None
try:
from app.services.user_memory_service import UserMemoryService
title, episodic_type = await UserMemoryService.generate_title_and_type_for_summary(
title, episodic_type = await generate_title_and_type_for_summary(
content=summary_text,
end_user_id=dialog.group_id
llm_client=llm_client
)
logger.info(f"Generated title and type for MemorySummary: title={title}, type={episodic_type}")
except Exception as e:

View File

@@ -260,17 +260,32 @@ class ForgettingStrategy:
)
# 生成标题和类型使用LLM
from app.services.user_memory_service import UserMemoryService
from app.core.memory.storage_services.extraction_engine.knowledge_extraction.memory_summary import generate_title_and_type_for_summary
# 获取 LLM 客户端
llm_client = None
if config_id is not None and db is not None:
try:
llm_client = await self._get_llm_client(db, config_id)
except Exception as e:
logger.warning(f"获取 LLM 客户端失败: {str(e)}")
# 生成标题和类型
try:
title, episodic_type = await UserMemoryService.generate_title_and_type_for_summary(
content=summary_text,
end_user_id=group_id
)
logger.info(f"成功为MemorySummary生成标题和类型: title={title}, type={episodic_type}")
if llm_client is not None:
title, episodic_type = await generate_title_and_type_for_summary(
content=summary_text,
llm_client=llm_client
)
logger.info(f"成功为MemorySummary生成标题和类型: title={title}, type={episodic_type}")
else:
logger.warning("LLM 客户端不可用,使用默认标题和类型")
title = "未命名"
episodic_type = "conversation"
except Exception as e:
logger.error(f"生成标题和类型失败,使用默认值: {str(e)}")
title = "未命名"
episodic_type = "其他"
episodic_type = "conversation"
# 计算继承的激活值和重要性(取较高值)
inherited_activation = max(statement_activation, entity_activation)

View File

@@ -110,7 +110,7 @@ class BaiduSearchTool(BuiltinTool):
execution_time = time.time() - start_time
return ToolResult.success_result(
data=result,
data=result["results"],
execution_time=execution_time
)

View File

@@ -95,7 +95,7 @@ class DateTimeTool(BuiltinTool):
execution_time = time.time() - start_time
return ToolResult.success_result(
data=result,
data=result["result_data"],
execution_time=execution_time
)
@@ -123,12 +123,14 @@ class DateTimeTool(BuiltinTool):
utc_now = datetime.now(timezone.utc)
return {
"datetime": now.strftime(output_format),
"timestamp": int(now.timestamp()),
"timezone": timezone_str,
"iso_format": now.isoformat(),
"timestamp_ms": int(now.timestamp() * 1000),
"utc_datetime": utc_now.strftime(output_format)
"result_data": {
"datetime": now.strftime(output_format),
"timestamp": int(now.timestamp()),
"timestamp_ms": int(now.timestamp() * 1000),
"utc_datetime": utc_now.strftime(output_format),
}
}
@staticmethod
@@ -148,7 +150,8 @@ class DateTimeTool(BuiltinTool):
"original": input_value,
"formatted": dt.strftime(output_format),
"timestamp": int(dt.timestamp()),
"iso_format": dt.isoformat()
"iso_format": dt.isoformat(),
"result_data": dt.strftime(output_format)
}
@staticmethod
@@ -189,7 +192,8 @@ class DateTimeTool(BuiltinTool):
"original_timezone": from_timezone,
"converted": converted_dt.strftime(output_format),
"converted_timezone": to_timezone,
"timestamp": int(converted_dt.timestamp())
"timestamp": int(converted_dt.timestamp()),
"result_data": converted_dt.strftime(output_format)
}
@staticmethod
@@ -219,7 +223,8 @@ class DateTimeTool(BuiltinTool):
"timestamp": timestamp,
"datetime": dt.strftime(output_format),
"timezone": timezone_str,
"iso_format": dt.isoformat()
"iso_format": dt.isoformat(),
"result_data": dt.strftime(output_format)
}
@staticmethod
@@ -249,7 +254,8 @@ class DateTimeTool(BuiltinTool):
"datetime": input_value,
"timezone": timezone_str,
"timestamp": int(dt.timestamp()),
"iso_format": dt.isoformat()
"iso_format": dt.isoformat(),
"result_data": int(dt.timestamp())
}
def _calculate_datetime(self, kwargs) -> dict:
@@ -287,7 +293,8 @@ class DateTimeTool(BuiltinTool):
"calculation": calculation,
"result": calculated_dt.strftime(output_format),
"timezone": timezone_str,
"timestamp": int(calculated_dt.timestamp())
"timestamp": int(calculated_dt.timestamp()),
"result_data": calculated_dt.strftime(output_format)
}
@staticmethod

View File

@@ -69,7 +69,7 @@ class JsonTool(BuiltinTool):
ToolParameter(
name="json_path",
type=ParameterType.STRING,
description="JSON路径表达式用于extract、insert、replace、delete、parse操作$.user.name或users[0].name",
description="JSON路径表达式用于insert、replace、delete、parse操作$.user.name或users[0].name",
required=False
),
ToolParameter(
@@ -136,7 +136,7 @@ class JsonTool(BuiltinTool):
execution_time = time.time() - start_time
return ToolResult.success_result(
data=result,
data=result["result_data"],
execution_time=execution_time
)
@@ -671,7 +671,8 @@ class JsonTool(BuiltinTool):
"success": True,
"value": current,
"value_type": type(current).__name__,
"value_json": json.dumps(current, indent=2, ensure_ascii=False) if isinstance(current, (dict, list)) else str(current)
"value_json": json.dumps(current, indent=2, ensure_ascii=False) if isinstance(current, (dict, list)) else str(current),
"result_data": json.dumps(current, indent=2, ensure_ascii=False) if isinstance(current, (dict, list)) else str(current)
}
except (KeyError, IndexError, TypeError) as e:
@@ -680,7 +681,8 @@ class JsonTool(BuiltinTool):
"json_path": json_path,
"success": False,
"error": str(e),
"value": None
"value": None,
"result_data": None
}
def _analyze_json_structure(self, data: Any, depth: int = 0) -> Dict[str, Any]:

View File

@@ -1,5 +1,7 @@
import json
import logging
import re
import uuid
from typing import Any
from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
@@ -25,10 +27,10 @@ class ToolNode(BaseNode):
# 获取租户ID和用户ID
tenant_id = self.get_variable("sys.tenant_id", state)
user_id = self.get_variable("sys.user_id", state)
workspace_id = self.get_variable("sys.workspace_id", state)
# 如果没有租户ID尝试从工作流ID获取
if not tenant_id:
workspace_id = self.get_variable("sys.workspace_id", state)
if workspace_id:
from app.repositories.tool_repository import ToolRepository
with get_db_read() as db:
@@ -63,21 +65,21 @@ class ToolNode(BaseNode):
tool_id=self.typed_config.tool_id,
parameters=rendered_parameters,
tenant_id=tenant_id,
user_id=user_id
user_id=uuid.UUID(user_id),
workspace_id=uuid.UUID(workspace_id)
)
if result.success:
logger.info(f"节点 {self.node_id} 工具执行成功")
return {
"success": True,
"data": result.data,
"data": result.data if isinstance(result.data, str) else json.dumps(result.data, ensure_ascii=False),
"error_code": "",
"execution_time": result.execution_time
}
else:
logger.error(f"节点 {self.node_id} 工具执行失败: {result.error}")
return {
"success": False,
"data": result.error,
"data": result.error if isinstance(result.error, str) else json.dumps(result.error, ensure_ascii=False),
"error_code": result.error_code,
"execution_time": result.execution_time
}