Merge branch 'develop' into refactor/memory_search
# Conflicts: # api/app/core/memory/storage_services/search/__init__.py
This commit is contained in:
@@ -248,6 +248,35 @@ class RateLimiterService:
|
||||
def __init__(self):
|
||||
self.redis = aio_redis
|
||||
|
||||
async def check_tenant_rate_limit(self, tenant_id: uuid.UUID, limit: int) -> Tuple[bool, dict]:
|
||||
"""
|
||||
按 tenant_id 做 1 秒滑动窗口限速,限制值来自套餐配额 api_ops_rate_limit
|
||||
"""
|
||||
now = time.time()
|
||||
window_start = now - 1 # 1 秒窗口
|
||||
key = f"rate_limit:tenant_qps:{tenant_id}"
|
||||
|
||||
async with self.redis.pipeline() as pipe:
|
||||
# 清理 1 秒前的旧记录
|
||||
pipe.zremrangebyscore(key, 0, window_start)
|
||||
# 加入当前请求(score=时间戳,member=时间戳+随机数保证唯一)
|
||||
pipe.zadd(key, {f"{now}:{uuid.uuid4().hex}": now})
|
||||
# 统计窗口内请求数
|
||||
pipe.zcard(key)
|
||||
# 设置 key 过期(2 秒后自动清理)
|
||||
pipe.expire(key, 2)
|
||||
results = await pipe.execute()
|
||||
|
||||
current = results[2]
|
||||
remaining = max(0, limit - current)
|
||||
reset_time = int(now) + 1
|
||||
|
||||
return current <= limit, {
|
||||
"limit": limit,
|
||||
"remaining": remaining,
|
||||
"reset": reset_time,
|
||||
}
|
||||
|
||||
async def check_qps(self, api_key_id: uuid.UUID, limit: int) -> Tuple[bool, dict]:
|
||||
"""
|
||||
检查QPS限制
|
||||
|
||||
@@ -26,6 +26,7 @@ from app.services.model_service import ModelApiKeyService
|
||||
from app.services.multi_agent_orchestrator import MultiAgentOrchestrator
|
||||
from app.services.multimodal_service import MultimodalService
|
||||
from app.services.workflow_service import WorkflowService
|
||||
from app.models.file_metadata_model import FileMetadata
|
||||
|
||||
logger = get_business_logger()
|
||||
|
||||
@@ -119,6 +120,7 @@ class AppChatService:
|
||||
tools=tools,
|
||||
deep_thinking=model_parameters.get("deep_thinking", False),
|
||||
thinking_budget_tokens=model_parameters.get("thinking_budget_tokens"),
|
||||
json_output=model_parameters.get("json_output", False),
|
||||
capability=api_key_obj.capability or [],
|
||||
)
|
||||
|
||||
@@ -218,11 +220,29 @@ class AppChatService:
|
||||
"reasoning_content": result.get("reasoning_content")
|
||||
}
|
||||
if files:
|
||||
local_ids = [f.upload_file_id for f in files
|
||||
if f.transfer_method.value == "local_file" and f.upload_file_id
|
||||
and (not f.name or not f.size)]
|
||||
meta_map = {}
|
||||
if local_ids:
|
||||
rows = self.db.query(FileMetadata).filter(
|
||||
FileMetadata.id.in_(local_ids),
|
||||
FileMetadata.status == "completed"
|
||||
).all()
|
||||
meta_map = {str(r.id): r for r in rows}
|
||||
for f in files:
|
||||
# url = await MultimodalService(self.db).get_file_url(f)
|
||||
name, size = f.name, f.size
|
||||
if f.transfer_method.value == "local_file" and f.upload_file_id and (not name or not size):
|
||||
meta = meta_map.get(str(f.upload_file_id))
|
||||
if meta:
|
||||
name = name or meta.file_name
|
||||
size = size or meta.file_size
|
||||
human_meta["files"].append({
|
||||
"type": f.type,
|
||||
"url": f.url
|
||||
"url": f.url,
|
||||
"name": name,
|
||||
"size": size,
|
||||
"file_type": f.file_type,
|
||||
})
|
||||
|
||||
if processed_files:
|
||||
@@ -373,6 +393,7 @@ class AppChatService:
|
||||
streaming=True,
|
||||
deep_thinking=model_parameters.get("deep_thinking", False),
|
||||
thinking_budget_tokens=model_parameters.get("thinking_budget_tokens"),
|
||||
json_output=model_parameters.get("json_output", False),
|
||||
capability=api_key_obj.capability or [],
|
||||
)
|
||||
|
||||
@@ -509,10 +530,29 @@ class AppChatService:
|
||||
}
|
||||
|
||||
if files:
|
||||
local_ids = [f.upload_file_id for f in files
|
||||
if f.transfer_method.value == "local_file" and f.upload_file_id
|
||||
and (not f.name or not f.size)]
|
||||
meta_map = {}
|
||||
if local_ids:
|
||||
rows = self.db.query(FileMetadata).filter(
|
||||
FileMetadata.id.in_(local_ids),
|
||||
FileMetadata.status == "completed"
|
||||
).all()
|
||||
meta_map = {str(r.id): r for r in rows}
|
||||
for f in files:
|
||||
name, size = f.name, f.size
|
||||
if f.transfer_method.value == "local_file" and f.upload_file_id and (not name or not size):
|
||||
meta = meta_map.get(str(f.upload_file_id))
|
||||
if meta:
|
||||
name = name or meta.file_name
|
||||
size = size or meta.file_size
|
||||
human_meta["files"].append({
|
||||
"type": f.type,
|
||||
"url": f.url
|
||||
"url": f.url,
|
||||
"name": name,
|
||||
"size": size,
|
||||
"file_type": f.file_type,
|
||||
})
|
||||
if processed_files:
|
||||
human_meta["history_files"] = {
|
||||
|
||||
@@ -14,12 +14,14 @@ from app.models.app_model import App, AppType
|
||||
from app.models.appshare_model import AppShare
|
||||
from app.models.app_release_model import AppRelease
|
||||
from app.models.knowledge_model import Knowledge
|
||||
from app.models.knowledgeshare_model import KnowledgeShare
|
||||
from app.models.models_model import ModelConfig
|
||||
from app.models.tool_model import ToolConfig as ToolConfigModel
|
||||
from app.models.skill_model import Skill
|
||||
from app.models.workflow_model import WorkflowConfig
|
||||
from app.services.workflow_service import WorkflowService
|
||||
from app.core.workflow.adapters.memory_bear.memory_bear_adapter import MemoryBearAdapter
|
||||
from app.core.workflow.nodes.enums import NodeType
|
||||
from app.models.memory_config_model import MemoryConfig as MemoryConfigModel
|
||||
|
||||
|
||||
@@ -73,15 +75,14 @@ class AppDslService:
|
||||
AppType.MULTI_AGENT: "multi_agent_config",
|
||||
AppType.WORKFLOW: "workflow"
|
||||
}.get(app.type, "config")
|
||||
config_data = self._enrich_release_config(app.type, release.config or {})
|
||||
config_data = self._enrich_release_config(app.type, release.config or {}, release.default_model_config_id)
|
||||
dsl = {**meta, "app": app_meta, config_key: config_data}
|
||||
return yaml.dump(dsl, default_flow_style=False, allow_unicode=True), f"{release.name}_v{release.version_name}.yaml"
|
||||
|
||||
def _enrich_release_config(self, app_type: str, cfg: dict) -> dict:
|
||||
def _enrich_release_config(self, app_type: str, cfg: dict, default_model_config_id=None) -> dict:
|
||||
if app_type == AppType.AGENT:
|
||||
enriched = {**cfg}
|
||||
if "default_model_config_id" in cfg:
|
||||
enriched["default_model_config_ref"] = self._model_ref(cfg["default_model_config_id"])
|
||||
enriched["default_model_config_ref"] = self._model_ref(default_model_config_id)
|
||||
if "knowledge_retrieval" in cfg:
|
||||
enriched["knowledge_retrieval"] = self._enrich_knowledge_retrieval(cfg["knowledge_retrieval"])
|
||||
if "tools" in cfg:
|
||||
@@ -91,8 +92,7 @@ class AppDslService:
|
||||
return enriched
|
||||
if app_type == AppType.MULTI_AGENT:
|
||||
enriched = {**cfg}
|
||||
if "default_model_config_id" in cfg:
|
||||
enriched["default_model_config_ref"] = self._model_ref(cfg["default_model_config_id"])
|
||||
enriched["default_model_config_ref"] = self._model_ref(default_model_config_id)
|
||||
if "master_agent_id" in cfg:
|
||||
enriched["master_agent_ref"] = self._release_ref(cfg["master_agent_id"])
|
||||
if "sub_agents" in cfg:
|
||||
@@ -229,8 +229,11 @@ class AppDslService:
|
||||
workspace_id: uuid.UUID,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
app_id: Optional[uuid.UUID] = None,
|
||||
) -> tuple[App, list[str]]:
|
||||
"""解析 DSL,创建应用及配置,返回 (new_app, warnings)"""
|
||||
"""解析 DSL,创建或覆盖应用配置,返回 (app, warnings)。
|
||||
app_id 不为空时:校验类型一致后覆盖配置;为空时创建新应用。
|
||||
"""
|
||||
app_meta = dsl.get("app", {})
|
||||
app_type = app_meta.get("type")
|
||||
if app_type not in (AppType.AGENT, AppType.MULTI_AGENT, AppType.WORKFLOW):
|
||||
@@ -239,6 +242,9 @@ class AppDslService:
|
||||
warnings: list[str] = []
|
||||
now = datetime.datetime.now()
|
||||
|
||||
if app_id is not None:
|
||||
return self._overwrite_dsl(dsl, app_id, app_type, workspace_id, tenant_id, warnings, now)
|
||||
|
||||
new_app = App(
|
||||
id=uuid.uuid4(),
|
||||
workspace_id=workspace_id,
|
||||
@@ -258,11 +264,57 @@ class AppDslService:
|
||||
self.db.add(new_app)
|
||||
self.db.flush()
|
||||
|
||||
self._write_config(new_app.id, app_type, dsl, workspace_id, tenant_id, warnings, now, create=True)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(new_app)
|
||||
return new_app, warnings
|
||||
|
||||
def _overwrite_dsl(
|
||||
self,
|
||||
dsl: dict,
|
||||
app_id: uuid.UUID,
|
||||
app_type: str,
|
||||
workspace_id: uuid.UUID,
|
||||
tenant_id: uuid.UUID,
|
||||
warnings: list,
|
||||
now: datetime.datetime,
|
||||
) -> tuple[App, list[str]]:
|
||||
"""覆盖已有应用的配置,类型不一致时抛出异常"""
|
||||
app = self.db.query(App).filter(
|
||||
App.id == app_id,
|
||||
App.workspace_id == workspace_id,
|
||||
App.is_active.is_(True)
|
||||
).first()
|
||||
if not app:
|
||||
raise ResourceNotFoundException("应用", str(app_id))
|
||||
if app.type != app_type:
|
||||
raise BusinessException(
|
||||
f"YAML 类型 '{app_type}' 与应用类型 '{app.type}' 不一致,无法导入",
|
||||
BizCode.BAD_REQUEST
|
||||
)
|
||||
|
||||
self._write_config(app_id, app_type, dsl, workspace_id, tenant_id, warnings, now, create=False)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(app)
|
||||
return app, warnings
|
||||
|
||||
def _write_config(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
app_type: str,
|
||||
dsl: dict,
|
||||
workspace_id: uuid.UUID,
|
||||
tenant_id: uuid.UUID,
|
||||
warnings: list,
|
||||
now: datetime.datetime,
|
||||
create: bool,
|
||||
) -> None:
|
||||
"""写入(新建或覆盖)应用配置"""
|
||||
if app_type == AppType.AGENT:
|
||||
cfg = dsl.get("agent_config") or {}
|
||||
self.db.add(AgentConfig(
|
||||
id=uuid.uuid4(),
|
||||
app_id=new_app.id,
|
||||
fields = dict(
|
||||
system_prompt=cfg.get("system_prompt"),
|
||||
model_parameters=cfg.get("model_parameters"),
|
||||
default_model_config_id=self._resolve_model(cfg.get("default_model_config_ref"), tenant_id, warnings),
|
||||
@@ -272,16 +324,21 @@ class AppDslService:
|
||||
tools=self._resolve_tools(cfg.get("tools", []), tenant_id, warnings),
|
||||
skills=self._resolve_skills(cfg.get("skills", {}), tenant_id, warnings),
|
||||
features=cfg.get("features", {}),
|
||||
is_active=True,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
))
|
||||
)
|
||||
if create:
|
||||
self.db.add(AgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||
else:
|
||||
existing = self.db.query(AgentConfig).filter(AgentConfig.app_id == app_id).first()
|
||||
if existing:
|
||||
for k, v in fields.items():
|
||||
setattr(existing, k, v)
|
||||
else:
|
||||
self.db.add(AgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||
|
||||
elif app_type == AppType.MULTI_AGENT:
|
||||
cfg = dsl.get("multi_agent_config") or {}
|
||||
self.db.add(MultiAgentConfig(
|
||||
id=uuid.uuid4(),
|
||||
app_id=new_app.id,
|
||||
fields = dict(
|
||||
orchestration_mode=cfg.get("orchestration_mode", "collaboration"),
|
||||
master_agent_name=cfg.get("master_agent_name"),
|
||||
model_parameters=cfg.get("model_parameters"),
|
||||
@@ -291,13 +348,24 @@ class AppDslService:
|
||||
routing_rules=self._resolve_routing_rules(cfg.get("routing_rules"), warnings),
|
||||
execution_config=cfg.get("execution_config", {}),
|
||||
aggregation_strategy=cfg.get("aggregation_strategy", "merge"),
|
||||
is_active=True,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
))
|
||||
)
|
||||
if create:
|
||||
self.db.add(MultiAgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||
else:
|
||||
existing = self.db.query(MultiAgentConfig).filter(MultiAgentConfig.app_id == app_id).first()
|
||||
if existing:
|
||||
for k, v in fields.items():
|
||||
setattr(existing, k, v)
|
||||
else:
|
||||
self.db.add(MultiAgentConfig(id=uuid.uuid4(), app_id=app_id, is_active=True, created_at=now, **fields))
|
||||
|
||||
elif app_type == AppType.WORKFLOW:
|
||||
adapter = MemoryBearAdapter(dsl)
|
||||
raw_wf = dsl.get("workflow") or {}
|
||||
raw_nodes = raw_wf.get("nodes") or []
|
||||
resolved_nodes = self._resolve_workflow_nodes(raw_nodes, tenant_id, workspace_id, warnings)
|
||||
resolved_dsl = {**dsl, "workflow": {**raw_wf, "nodes": resolved_nodes}}
|
||||
adapter = MemoryBearAdapter(resolved_dsl)
|
||||
if not adapter.validate_config():
|
||||
raise BusinessException("工作流配置格式无效", BizCode.BAD_REQUEST)
|
||||
result = adapter.parse_workflow()
|
||||
@@ -305,21 +373,39 @@ class AppDslService:
|
||||
warnings.append(f"[节点错误] {e.node_name or e.node_id}: {e.detail}")
|
||||
for w in result.warnings:
|
||||
warnings.append(f"[节点警告] {w.node_name or w.node_id}: {w.detail}")
|
||||
wf = dsl.get("workflow") or {}
|
||||
WorkflowService(self.db).create_workflow_config(
|
||||
app_id=new_app.id,
|
||||
nodes=[n.model_dump() for n in result.nodes],
|
||||
edges=[e.model_dump() for e in result.edges],
|
||||
variables=[v.model_dump() for v in result.variables],
|
||||
execution_config=wf.get("execution_config", {}),
|
||||
features=wf.get("features", {}),
|
||||
triggers=wf.get("triggers", []),
|
||||
validate=False,
|
||||
)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(new_app)
|
||||
return new_app, warnings
|
||||
wf_service = WorkflowService(self.db)
|
||||
if create:
|
||||
wf_service.create_workflow_config(
|
||||
app_id=app_id,
|
||||
nodes=[n.model_dump() for n in result.nodes],
|
||||
edges=[e.model_dump() for e in result.edges],
|
||||
variables=[v.model_dump() for v in result.variables],
|
||||
execution_config=raw_wf.get("execution_config", {}),
|
||||
features=raw_wf.get("features", {}),
|
||||
triggers=raw_wf.get("triggers", []),
|
||||
validate=False,
|
||||
)
|
||||
else:
|
||||
existing = self.db.query(WorkflowConfig).filter(WorkflowConfig.app_id == app_id).first()
|
||||
if existing:
|
||||
existing.nodes = [n.model_dump() for n in result.nodes]
|
||||
existing.edges = [e.model_dump() for e in result.edges]
|
||||
existing.variables = [v.model_dump() for v in result.variables]
|
||||
existing.execution_config = raw_wf.get("execution_config", {})
|
||||
existing.features = raw_wf.get("features", {})
|
||||
existing.triggers = raw_wf.get("triggers", [])
|
||||
existing.updated_at = now
|
||||
else:
|
||||
wf_service.create_workflow_config(
|
||||
app_id=app_id,
|
||||
nodes=[n.model_dump() for n in result.nodes],
|
||||
edges=[e.model_dump() for e in result.edges],
|
||||
variables=[v.model_dump() for v in result.variables],
|
||||
execution_config=raw_wf.get("execution_config", {}),
|
||||
features=raw_wf.get("features", {}),
|
||||
triggers=raw_wf.get("triggers", []),
|
||||
validate=False,
|
||||
)
|
||||
|
||||
def _unique_app_name(self, name: str, workspace_id: uuid.UUID, app_type: AppType) -> str:
|
||||
"""生成唯一应用名称,同时检查本空间自有应用和共享到本空间的应用"""
|
||||
@@ -365,27 +451,63 @@ class AppDslService:
|
||||
def _resolve_kb(self, ref: Optional[dict], workspace_id: uuid.UUID, warnings: list) -> Optional[str]:
|
||||
if not ref:
|
||||
return None
|
||||
kb = self.db.query(Knowledge).filter(
|
||||
Knowledge.workspace_id == workspace_id,
|
||||
Knowledge.name == ref.get("name")
|
||||
).first()
|
||||
if not kb:
|
||||
warnings.append(f"知识库 '{ref.get('name')}' 未匹配,已置空,请导入后手动配置")
|
||||
return str(kb.id) if kb else None
|
||||
kb_id = ref.get("id")
|
||||
if kb_id:
|
||||
try:
|
||||
kb_uuid = uuid.UUID(str(kb_id))
|
||||
kb_share = self.db.query(KnowledgeShare).filter(
|
||||
KnowledgeShare.target_workspace_id == workspace_id,
|
||||
KnowledgeShare.source_kb_id == kb_uuid
|
||||
).first()
|
||||
if kb_share:
|
||||
kb = self.db.query(Knowledge).filter(
|
||||
Knowledge.id == kb_share.target_kb_id
|
||||
).first()
|
||||
if kb and kb.status == 1:
|
||||
return str(kb_share.target_kb_id)
|
||||
kb = self.db.query(Knowledge).filter(
|
||||
Knowledge.workspace_id == workspace_id,
|
||||
Knowledge.id == kb_uuid,
|
||||
Knowledge.status == 1
|
||||
).first()
|
||||
if kb:
|
||||
return str(kb.id)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
warnings.append(f"知识库 '{kb_id}' 未匹配,已置空,请导入后手动配置")
|
||||
return None
|
||||
|
||||
def _resolve_tool(self, ref: Optional[dict], tenant_id: uuid.UUID, warnings: list) -> Optional[str]:
|
||||
if not ref:
|
||||
return None
|
||||
q = self.db.query(ToolConfigModel).filter(
|
||||
ToolConfigModel.tenant_id == tenant_id,
|
||||
ToolConfigModel.name == ref.get("name")
|
||||
)
|
||||
if ref.get("tool_type"):
|
||||
q = q.filter(ToolConfigModel.tool_type == ref["tool_type"])
|
||||
t = q.first()
|
||||
if not t:
|
||||
warnings.append(f"工具 '{ref.get('name')}' 未匹配,已置空,请导入后手动配置")
|
||||
return str(t.id) if t else None
|
||||
tool_id = ref.get("id")
|
||||
tool_name = ref.get("name")
|
||||
if tool_id:
|
||||
try:
|
||||
tool_uuid = uuid.UUID(str(tool_id))
|
||||
t = self.db.query(ToolConfigModel).filter(
|
||||
ToolConfigModel.id == tool_uuid,
|
||||
ToolConfigModel.tenant_id == tenant_id,
|
||||
ToolConfigModel.is_active.is_(True)
|
||||
).first()
|
||||
if t:
|
||||
return str(t.id)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
if tool_name:
|
||||
q = self.db.query(ToolConfigModel).filter(
|
||||
ToolConfigModel.tenant_id == tenant_id,
|
||||
ToolConfigModel.name == tool_name
|
||||
)
|
||||
if ref.get("tool_type"):
|
||||
q = q.filter(ToolConfigModel.tool_type == ref["tool_type"])
|
||||
t = q.first()
|
||||
if t:
|
||||
return str(t.id)
|
||||
warnings.append(f"工具 '{tool_name}' 未匹配,已置空,请导入后手动配置")
|
||||
else:
|
||||
warnings.append(f"工具 '{tool_id}' 未匹配,已置空,请导入后手动配置")
|
||||
return None
|
||||
|
||||
def _resolve_release(self, ref: Optional[dict], warnings: list) -> Optional[uuid.UUID]:
|
||||
if not ref:
|
||||
@@ -427,6 +549,61 @@ class AppDslService:
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
def _resolve_workflow_nodes(self, nodes: list, tenant_id: uuid.UUID, workspace_id: uuid.UUID, warnings: list) -> list:
|
||||
"""解析工作流节点中的工具ID和知识库ID,匹配不到则清空配置"""
|
||||
resolved_nodes = []
|
||||
for node in nodes:
|
||||
node_type = node.get("type")
|
||||
config = dict(node.get("config") or {})
|
||||
node_label = node.get("name") or node.get("id")
|
||||
if node_type == NodeType.TOOL.value:
|
||||
tool_id = config.get("tool_id")
|
||||
if not tool_id:
|
||||
# tool_id 本身就是空,直接置空不重复 warning
|
||||
config["tool_id"] = None
|
||||
config["tool_parameters"] = {}
|
||||
else:
|
||||
tool_ref = {}
|
||||
if isinstance(tool_id, str) and len(tool_id) >= 36:
|
||||
try:
|
||||
uuid.UUID(tool_id)
|
||||
tool_ref["id"] = tool_id
|
||||
except ValueError:
|
||||
tool_ref["name"] = tool_id
|
||||
else:
|
||||
tool_ref["name"] = tool_id
|
||||
resolved_tool_id = self._resolve_tool(tool_ref, tenant_id, [])
|
||||
if resolved_tool_id:
|
||||
config["tool_id"] = resolved_tool_id
|
||||
else:
|
||||
warnings.append(f"[{node_label}] 工具 '{tool_id}' 未匹配,已置空,请导入后手动配置")
|
||||
config["tool_id"] = None
|
||||
config["tool_parameters"] = {}
|
||||
elif node_type == NodeType.KNOWLEDGE_RETRIEVAL.value:
|
||||
knowledge_bases = config.get("knowledge_bases") or []
|
||||
resolved_kbs = []
|
||||
for kb in knowledge_bases:
|
||||
kb_id = kb.get("kb_id")
|
||||
if not kb_id:
|
||||
continue
|
||||
kb_ref = {}
|
||||
if isinstance(kb_id, str) and len(kb_id) >= 36:
|
||||
try:
|
||||
uuid.UUID(kb_id)
|
||||
kb_ref["id"] = kb_id
|
||||
except ValueError:
|
||||
kb_ref["name"] = kb_id
|
||||
else:
|
||||
kb_ref["name"] = kb_id
|
||||
resolved_id = self._resolve_kb(kb_ref, workspace_id, [])
|
||||
if resolved_id:
|
||||
resolved_kbs.append({**kb, "kb_id": resolved_id})
|
||||
else:
|
||||
warnings.append(f"[{node_label}] 知识库 '{kb_id}' 未匹配,已移除,请导入后手动配置")
|
||||
config["knowledge_bases"] = resolved_kbs
|
||||
resolved_nodes.append({**node, "config": config})
|
||||
return resolved_nodes
|
||||
|
||||
def _resolve_knowledge_retrieval(self, kr: Optional[dict], workspace_id: uuid.UUID, warnings: list) -> Optional[dict]:
|
||||
if not kr:
|
||||
return kr
|
||||
|
||||
@@ -1452,6 +1452,32 @@ class AppService:
|
||||
logger.debug("配置不存在,返回默认模板", extra={"app_id": str(app_id)})
|
||||
return self._create_default_agent_config(app_id)
|
||||
|
||||
def get_default_model_parameters(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
) -> "ModelParameters":
|
||||
"""获取 Agent 默认模型参数(不修改数据库)
|
||||
|
||||
Args:
|
||||
app_id: 应用ID
|
||||
|
||||
Returns:
|
||||
ModelParameters: 默认模型参数
|
||||
"""
|
||||
logger.info("获取 Agent 默认模型参数", extra={"app_id": str(app_id)})
|
||||
|
||||
app = self._get_app_or_404(app_id)
|
||||
|
||||
if app.type != "agent":
|
||||
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||||
|
||||
from app.schemas.app_schema import ModelParameters
|
||||
default_model_parameters = ModelParameters()
|
||||
|
||||
logger.info("获取 Agent 默认模型参数成功", extra={"app_id": str(app_id)})
|
||||
return default_model_parameters
|
||||
|
||||
def _create_default_agent_config(self, app_id: uuid.UUID) -> AgentConfig:
|
||||
"""创建默认的 Agent 配置模板(不保存到数据库)
|
||||
|
||||
|
||||
@@ -544,7 +544,7 @@ class ConversationService:
|
||||
api_key=api_key,
|
||||
base_url=api_base,
|
||||
is_omni=is_omni,
|
||||
support_thinking="thinking" in (capability or []),
|
||||
capability=capability,
|
||||
),
|
||||
type=ModelType(model_type)
|
||||
)
|
||||
|
||||
@@ -599,6 +599,7 @@ class AgentRunService:
|
||||
tools=tools,
|
||||
deep_thinking=effective_params.get("deep_thinking", False),
|
||||
thinking_budget_tokens=effective_params.get("thinking_budget_tokens"),
|
||||
json_output=effective_params.get("json_output", False),
|
||||
capability=api_key_config.get("capability", []),
|
||||
)
|
||||
|
||||
@@ -855,6 +856,7 @@ class AgentRunService:
|
||||
streaming=True,
|
||||
deep_thinking=effective_params.get("deep_thinking", False),
|
||||
thinking_budget_tokens=effective_params.get("thinking_budget_tokens"),
|
||||
json_output=effective_params.get("json_output", False),
|
||||
capability=api_key_config.get("capability", []),
|
||||
)
|
||||
|
||||
@@ -1301,10 +1303,30 @@ class AgentRunService:
|
||||
"history_files": {}
|
||||
}
|
||||
if files:
|
||||
from app.models.file_metadata_model import FileMetadata
|
||||
local_ids = [f.upload_file_id for f in files
|
||||
if f.transfer_method.value == "local_file" and f.upload_file_id
|
||||
and (not f.name or not f.size)]
|
||||
meta_map = {}
|
||||
if local_ids:
|
||||
rows = self.db.query(FileMetadata).filter(
|
||||
FileMetadata.id.in_(local_ids),
|
||||
FileMetadata.status == "completed"
|
||||
).all()
|
||||
meta_map = {str(r.id): r for r in rows}
|
||||
for f in files:
|
||||
name, size = f.name, f.size
|
||||
if f.transfer_method.value == "local_file" and f.upload_file_id and (not name or not size):
|
||||
meta = meta_map.get(str(f.upload_file_id))
|
||||
if meta:
|
||||
name = name or meta.file_name
|
||||
size = size or meta.file_size
|
||||
human_meta["files"].append({
|
||||
"type": f.type,
|
||||
"url": f.url
|
||||
"url": f.url,
|
||||
"file_type": f.file_type,
|
||||
"name": name,
|
||||
"size": size
|
||||
})
|
||||
|
||||
# 保存 history_files,包含 provider 和 is_omni 信息
|
||||
|
||||
@@ -679,9 +679,9 @@ class EmotionAnalyticsService:
|
||||
|
||||
# 查询用户的实体和标签
|
||||
query = """
|
||||
MATCH (e:Entity)
|
||||
MATCH (e:ExtractedEntity)
|
||||
WHERE e.end_user_id = $end_user_id
|
||||
RETURN e.name as name, e.type as type
|
||||
RETURN e.name as name, e.entity_type as type
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
|
||||
@@ -34,6 +34,7 @@ from app.schemas.implicit_memory_schema import (
|
||||
UserMemorySummary,
|
||||
)
|
||||
from app.schemas.memory_config_schema import MemoryConfig
|
||||
from app.services.memory_base_service import MIN_MEMORY_SUMMARY_COUNT
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -379,12 +380,59 @@ class ImplicitMemoryService:
|
||||
raise
|
||||
|
||||
|
||||
def _build_empty_profile(self) -> dict:
|
||||
"""构建 MemorySummary 不足时返回的固定空白画像数据"""
|
||||
now_ms = int(datetime.utcnow().timestamp() * 1000)
|
||||
insufficient = "Insufficient data for analysis"
|
||||
|
||||
def _empty_dimension(name: str) -> dict:
|
||||
return {
|
||||
"evidence": [insufficient],
|
||||
"reasoning": f"No clear evidence found for {name} dimension",
|
||||
"percentage": 0.0,
|
||||
"dimension_name": name,
|
||||
"confidence_level": 20,
|
||||
}
|
||||
|
||||
def _empty_category(name: str) -> dict:
|
||||
return {
|
||||
"evidence": [insufficient],
|
||||
"percentage": 25.0,
|
||||
"category_name": name,
|
||||
"trending_direction": None,
|
||||
}
|
||||
|
||||
return {
|
||||
"habits": [],
|
||||
"portrait": {
|
||||
"aesthetic": _empty_dimension("aesthetic"),
|
||||
"creativity": _empty_dimension("creativity"),
|
||||
"literature": _empty_dimension("literature"),
|
||||
"technology": _empty_dimension("technology"),
|
||||
"historical_trends": None,
|
||||
"analysis_timestamp": now_ms,
|
||||
"total_summaries_analyzed": 0,
|
||||
},
|
||||
"preferences": [],
|
||||
"interest_areas": {
|
||||
"art": _empty_category("art"),
|
||||
"tech": _empty_category("tech"),
|
||||
"music": _empty_category("music"),
|
||||
"lifestyle": _empty_category("lifestyle"),
|
||||
"analysis_timestamp": now_ms,
|
||||
"total_summaries_analyzed": 0,
|
||||
},
|
||||
}
|
||||
|
||||
async def generate_complete_profile(
|
||||
self,
|
||||
user_id: str
|
||||
) -> dict:
|
||||
"""生成完整的用户画像(包含所有4个模块)
|
||||
|
||||
需要该用户的 MemorySummary 节点数量 >= 5 才会真正调用 LLM 生成画像,
|
||||
否则返回固定的空白画像数据。
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
@@ -394,6 +442,16 @@ class ImplicitMemoryService:
|
||||
logger.info(f"生成完整用户画像: user={user_id}")
|
||||
|
||||
try:
|
||||
# 前置检查:查询该用户有效的 MemorySummary 节点数量(排除孤立节点)
|
||||
from app.services.memory_base_service import MemoryBaseService
|
||||
base_service = MemoryBaseService()
|
||||
memory_summary_count = await base_service.get_valid_memory_summary_count(user_id)
|
||||
logger.info(f"用户 MemorySummary 节点数量: {memory_summary_count} (user={user_id})")
|
||||
|
||||
if memory_summary_count < MIN_MEMORY_SUMMARY_COUNT:
|
||||
logger.info(f"MemorySummary 数量不足 {MIN_MEMORY_SUMMARY_COUNT}(当前 {memory_summary_count}),返回空白画像: user={user_id}")
|
||||
return self._build_empty_profile()
|
||||
|
||||
# 并行调用4个分析方法
|
||||
preferences, portrait, interest_areas, habits = await asyncio.gather(
|
||||
self.get_preference_tags(user_id=user_id),
|
||||
|
||||
@@ -2,11 +2,14 @@ import uuid
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.user_model import User
|
||||
from app.models.knowledge_model import Knowledge
|
||||
from app.models.workspace_model import Workspace
|
||||
from app.models.models_model import ModelConfig
|
||||
from app.schemas.knowledge_schema import KnowledgeCreate, KnowledgeUpdate
|
||||
from app.repositories import knowledge_repository
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.repositories.model_repository import ModelConfigRepository
|
||||
from app.models.models_model import ModelType
|
||||
|
||||
# Obtain a dedicated logger for business logic
|
||||
business_logger = get_business_logger()
|
||||
|
||||
|
||||
@@ -60,13 +63,57 @@ def create_knowledge(
|
||||
db: Session, knowledge: KnowledgeCreate, current_user: User
|
||||
) -> Knowledge:
|
||||
business_logger.info(f"Create a knowledge base: {knowledge.name}, creator: {current_user.username}")
|
||||
|
||||
|
||||
try:
|
||||
knowledge.created_by = current_user.id
|
||||
if knowledge.workspace_id is None:
|
||||
knowledge.workspace_id = current_user.current_workspace_id
|
||||
if knowledge.parent_id is None:
|
||||
knowledge.parent_id = knowledge.workspace_id
|
||||
|
||||
workspace = db.query(Workspace).filter(Workspace.id == knowledge.workspace_id).first()
|
||||
if not workspace:
|
||||
raise Exception(f"Workspace {knowledge.workspace_id} not found")
|
||||
|
||||
tenant_id = workspace.tenant_id
|
||||
|
||||
if not knowledge.embedding_id:
|
||||
embedding_models = ModelConfigRepository.get_by_type(
|
||||
db=db, model_types=[ModelType.EMBEDDING], tenant_id=tenant_id, is_active=True
|
||||
)
|
||||
if embedding_models:
|
||||
knowledge.embedding_id = embedding_models[0].id
|
||||
business_logger.debug(f"Auto-bind embedding model: {embedding_models[0].id}")
|
||||
|
||||
if not knowledge.reranker_id:
|
||||
rerank_models = ModelConfigRepository.get_by_type(
|
||||
db=db, model_types=[ModelType.RERANK], tenant_id=tenant_id, is_active=True
|
||||
)
|
||||
if rerank_models:
|
||||
knowledge.reranker_id = rerank_models[0].id
|
||||
business_logger.debug(f"Auto-bind rerank model: {rerank_models[0].id}")
|
||||
|
||||
if not knowledge.llm_id:
|
||||
llm_models = ModelConfigRepository.get_by_type(
|
||||
db=db, model_types=[ModelType.LLM, ModelType.CHAT], tenant_id=tenant_id, is_active=True
|
||||
)
|
||||
if llm_models:
|
||||
knowledge.llm_id = llm_models[0].id
|
||||
business_logger.debug(f"Auto-bind llm model: {llm_models[0].id}")
|
||||
|
||||
if not knowledge.image2text_id:
|
||||
image2text_models = db.query(ModelConfig).filter(
|
||||
ModelConfig.tenant_id == tenant_id,
|
||||
ModelConfig.type.in_([ModelType.CHAT.value]),
|
||||
ModelConfig.capability.contains(["vision"]),
|
||||
ModelConfig.is_active == True,
|
||||
ModelConfig.is_composite == False
|
||||
).order_by(ModelConfig.created_at.desc()).all()
|
||||
if not image2text_models:
|
||||
raise Exception("租户下没有可用的视觉模型,创建知识库失败")
|
||||
knowledge.image2text_id = image2text_models[0].id
|
||||
business_logger.debug(f"Auto-bind image2text model: {image2text_models[0].id}")
|
||||
|
||||
business_logger.debug(f"Start creating the knowledge base: {knowledge.name}")
|
||||
db_knowledge = knowledge_repository.create_knowledge(
|
||||
db=db, knowledge=knowledge
|
||||
|
||||
@@ -415,9 +415,11 @@ class LLMRouter:
|
||||
api_key=api_key_config.api_key,
|
||||
base_url=api_key_config.api_base,
|
||||
is_omni=api_key_config.is_omni,
|
||||
support_thinking="thinking" in (api_key_config.capability or []),
|
||||
temperature=0.3,
|
||||
max_tokens=500
|
||||
capability=api_key_config.capability,
|
||||
extra_params={
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 500
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(f"创建 LLM 实例 - Provider: {api_key_config.provider}, Model: {api_key_config.model_name}")
|
||||
|
||||
@@ -393,7 +393,7 @@ class MasterAgentRouter:
|
||||
api_key=api_key_config.api_key,
|
||||
base_url=api_key_config.api_base,
|
||||
is_omni=api_key_config.is_omni,
|
||||
support_thinking="thinking" in (api_key_config.capability or []),
|
||||
capability=api_key_config.capability,
|
||||
extra_params = extra_params
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ This service validates inputs and delegates to MemoryAgentService for core memor
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.exceptions import BusinessException, ResourceNotFoundException
|
||||
from app.core.logging_config import get_logger
|
||||
@@ -15,7 +17,6 @@ from app.models.app_model import App
|
||||
from app.models.end_user_model import EndUser
|
||||
from app.schemas.memory_config_schema import ConfigurationError
|
||||
from app.services.memory_agent_service import MemoryAgentService
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -124,7 +125,7 @@ class MemoryAPIService:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update memory_config_id for end_user {end_user_id}: {e}")
|
||||
|
||||
async def write_memory(
|
||||
def write_memory(
|
||||
self,
|
||||
workspace_id: uuid.UUID,
|
||||
end_user_id: str,
|
||||
@@ -133,27 +134,28 @@ class MemoryAPIService:
|
||||
storage_type: str = "neo4j",
|
||||
user_rag_memory_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Write memory with validation.
|
||||
|
||||
"""Submit a memory write task via Celery.
|
||||
|
||||
Validates end_user exists and belongs to workspace, updates the end user's
|
||||
memory_config_id, then delegates to MemoryAgentService.write_memory.
|
||||
|
||||
memory_config_id, then dispatches write_message_task to Celery for async
|
||||
processing with per-user fair locking.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace ID for resource validation
|
||||
end_user_id: End user identifier (used as end_user_id)
|
||||
end_user_id: End user identifier
|
||||
message: Message content to store
|
||||
config_id: Memory configuration ID (required)
|
||||
storage_type: Storage backend (neo4j or rag)
|
||||
user_rag_memory_id: Optional RAG memory ID
|
||||
|
||||
|
||||
Returns:
|
||||
Dict with status and end_user_id
|
||||
|
||||
Dict with task_id, status, and end_user_id
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If end_user not found
|
||||
BusinessException: If end_user not in authorized workspace or write fails
|
||||
BusinessException: If validation fails
|
||||
"""
|
||||
logger.info(f"Writing memory for end_user: {end_user_id}, workspace: {workspace_id}")
|
||||
logger.info(f"Submitting memory write for end_user: {end_user_id}, workspace: {workspace_id}")
|
||||
|
||||
# Validate end_user exists and belongs to workspace
|
||||
self.validate_end_user(end_user_id, workspace_id)
|
||||
@@ -161,9 +163,120 @@ class MemoryAPIService:
|
||||
# Update end user's memory_config_id
|
||||
self._update_end_user_config(end_user_id, config_id)
|
||||
|
||||
# Convert to message list format expected by write_message_task
|
||||
messages = message if isinstance(message, list) else [{"role": "user", "content": message}]
|
||||
|
||||
from app.tasks import write_message_task
|
||||
task = write_message_task.delay(
|
||||
end_user_id,
|
||||
messages,
|
||||
config_id,
|
||||
storage_type,
|
||||
user_rag_memory_id or "",
|
||||
)
|
||||
|
||||
logger.info(f"Memory write task submitted: task_id={task.id}, end_user_id={end_user_id}")
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"status": "PENDING",
|
||||
"end_user_id": end_user_id,
|
||||
}
|
||||
|
||||
def read_memory(
|
||||
self,
|
||||
workspace_id: uuid.UUID,
|
||||
end_user_id: str,
|
||||
message: str,
|
||||
search_switch: str = "0",
|
||||
config_id: str = "",
|
||||
storage_type: str = "neo4j",
|
||||
user_rag_memory_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Submit a memory read task via Celery.
|
||||
|
||||
Validates end_user exists and belongs to workspace, updates the end user's
|
||||
memory_config_id, then dispatches read_message_task to Celery for async processing.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace ID for resource validation
|
||||
end_user_id: End user identifier
|
||||
message: Query message
|
||||
search_switch: Search mode (0=deep search with verification, 1=deep search, 2=fast search)
|
||||
config_id: Memory configuration ID (required)
|
||||
storage_type: Storage backend (neo4j or rag)
|
||||
user_rag_memory_id: Optional RAG memory ID
|
||||
|
||||
Returns:
|
||||
Dict with task_id, status, and end_user_id
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If end_user not found
|
||||
BusinessException: If validation fails
|
||||
"""
|
||||
logger.info(f"Submitting memory read for end_user: {end_user_id}, workspace: {workspace_id}")
|
||||
|
||||
# Validate end_user exists and belongs to workspace
|
||||
self.validate_end_user(end_user_id, workspace_id)
|
||||
|
||||
# Update end user's memory_config_id
|
||||
self._update_end_user_config(end_user_id, config_id)
|
||||
|
||||
from app.tasks import read_message_task
|
||||
task = read_message_task.delay(
|
||||
end_user_id,
|
||||
message,
|
||||
[], # history
|
||||
search_switch,
|
||||
config_id,
|
||||
storage_type,
|
||||
user_rag_memory_id or "",
|
||||
)
|
||||
|
||||
logger.info(f"Memory read task submitted: task_id={task.id}, end_user_id={end_user_id}")
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"status": "PENDING",
|
||||
"end_user_id": end_user_id,
|
||||
}
|
||||
|
||||
async def write_memory_sync(
|
||||
self,
|
||||
workspace_id: uuid.UUID,
|
||||
end_user_id: str,
|
||||
message: str,
|
||||
config_id: str,
|
||||
storage_type: str = "neo4j",
|
||||
user_rag_memory_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Write memory synchronously (inline, no Celery).
|
||||
|
||||
Validates end_user, then calls MemoryAgentService.write_memory directly.
|
||||
Blocks until the write completes. Use for cases where the caller needs
|
||||
immediate confirmation.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace ID for resource validation
|
||||
end_user_id: End user identifier
|
||||
message: Message content to store
|
||||
config_id: Memory configuration ID (required)
|
||||
storage_type: Storage backend (neo4j or rag)
|
||||
user_rag_memory_id: Optional RAG memory ID
|
||||
|
||||
Returns:
|
||||
Dict with status and end_user_id
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If end_user not found
|
||||
BusinessException: If write fails
|
||||
"""
|
||||
logger.info(f"Writing memory (sync) for end_user: {end_user_id}, workspace: {workspace_id}")
|
||||
|
||||
self.validate_end_user(end_user_id, workspace_id)
|
||||
self._update_end_user_config(end_user_id, config_id)
|
||||
|
||||
try:
|
||||
# Delegate to MemoryAgentService
|
||||
# Convert string message to list[dict] format expected by MemoryAgentService
|
||||
messages = message if isinstance(message, list) else [{"role": "user", "content": message}]
|
||||
result = await MemoryAgentService().write_memory(
|
||||
end_user_id=end_user_id,
|
||||
@@ -174,11 +287,8 @@ class MemoryAPIService:
|
||||
user_rag_memory_id=user_rag_memory_id or "",
|
||||
)
|
||||
|
||||
logger.info(f"Memory write successful for end_user: {end_user_id}")
|
||||
logger.info(f"Memory write (sync) successful for end_user: {end_user_id}")
|
||||
|
||||
# result may be a string "success" or a dict with a "status" key
|
||||
# Preserve the full dict so callers don't silently lose extra fields
|
||||
# (e.g. error codes, metadata) returned by MemoryAgentService.
|
||||
if isinstance(result, dict):
|
||||
return {
|
||||
**result,
|
||||
@@ -192,20 +302,17 @@ class MemoryAPIService:
|
||||
|
||||
except ConfigurationError as e:
|
||||
logger.error(f"Memory configuration error for end_user {end_user_id}: {e}")
|
||||
raise BusinessException(
|
||||
message=str(e),
|
||||
code=BizCode.MEMORY_CONFIG_NOT_FOUND
|
||||
)
|
||||
raise BusinessException(message=str(e), code=BizCode.MEMORY_CONFIG_NOT_FOUND)
|
||||
except BusinessException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Memory write failed for end_user {end_user_id}: {e}")
|
||||
logger.error(f"Memory write (sync) failed for end_user {end_user_id}: {e}")
|
||||
raise BusinessException(
|
||||
message=f"Memory write failed: {str(e)}",
|
||||
code=BizCode.MEMORY_WRITE_FAILED
|
||||
)
|
||||
|
||||
async def read_memory(
|
||||
async def read_memory_sync(
|
||||
self,
|
||||
workspace_id: uuid.UUID,
|
||||
end_user_id: str,
|
||||
@@ -215,37 +322,34 @@ class MemoryAPIService:
|
||||
storage_type: str = "neo4j",
|
||||
user_rag_memory_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Read memory with validation.
|
||||
|
||||
Validates end_user exists and belongs to workspace, updates the end user's
|
||||
memory_config_id, then delegates to MemoryAgentService.read_memory.
|
||||
|
||||
"""Read memory synchronously (inline, no Celery).
|
||||
|
||||
Validates end_user, then calls MemoryAgentService.read_memory directly.
|
||||
Blocks until the read completes. Use for cases where the caller needs
|
||||
the answer immediately.
|
||||
|
||||
Args:
|
||||
workspace_id: Workspace ID for resource validation
|
||||
end_user_id: End user identifier (used as end_user_id)
|
||||
end_user_id: End user identifier
|
||||
message: Query message
|
||||
search_switch: Search mode (0=deep search with verification, 1=deep search, 2=fast search)
|
||||
config_id: Memory configuration ID (required)
|
||||
storage_type: Storage backend (neo4j or rag)
|
||||
user_rag_memory_id: Optional RAG memory ID
|
||||
|
||||
|
||||
Returns:
|
||||
Dict with answer, intermediate_outputs, and end_user_id
|
||||
|
||||
|
||||
Raises:
|
||||
ResourceNotFoundException: If end_user not found
|
||||
BusinessException: If end_user not in authorized workspace or read fails
|
||||
BusinessException: If read fails
|
||||
"""
|
||||
logger.info(f"Reading memory for end_user: {end_user_id}, workspace: {workspace_id}")
|
||||
logger.info(f"Reading memory (sync) for end_user: {end_user_id}, workspace: {workspace_id}")
|
||||
|
||||
# Validate end_user exists and belongs to workspace
|
||||
self.validate_end_user(end_user_id, workspace_id)
|
||||
|
||||
# Update end user's memory_config_id
|
||||
self._update_end_user_config(end_user_id, config_id)
|
||||
|
||||
try:
|
||||
# Delegate to MemoryAgentService
|
||||
result = await MemoryAgentService().read_memory(
|
||||
end_user_id=end_user_id,
|
||||
message=message,
|
||||
@@ -257,7 +361,7 @@ class MemoryAPIService:
|
||||
user_rag_memory_id=user_rag_memory_id or ""
|
||||
)
|
||||
|
||||
logger.info(f"Memory read successful for end_user: {end_user_id}")
|
||||
logger.info(f"Memory read (sync) successful for end_user: {end_user_id}")
|
||||
|
||||
return {
|
||||
"answer": result.get("answer", ""),
|
||||
@@ -267,14 +371,11 @@ class MemoryAPIService:
|
||||
|
||||
except ConfigurationError as e:
|
||||
logger.error(f"Memory configuration error for end_user {end_user_id}: {e}")
|
||||
raise BusinessException(
|
||||
message=str(e),
|
||||
code=BizCode.MEMORY_CONFIG_NOT_FOUND
|
||||
)
|
||||
raise BusinessException(message=str(e), code=BizCode.MEMORY_CONFIG_NOT_FOUND)
|
||||
except BusinessException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Memory read failed for end_user {end_user_id}: {e}")
|
||||
logger.error(f"Memory read (sync) failed for end_user {end_user_id}: {e}")
|
||||
raise BusinessException(
|
||||
message=f"Memory read failed: {str(e)}",
|
||||
code=BizCode.MEMORY_READ_FAILED
|
||||
|
||||
@@ -265,12 +265,50 @@ async def Translation_English(modid, text, fields=None):
|
||||
# 其他类型(数字、布尔值、None等):原样返回
|
||||
else:
|
||||
return text
|
||||
# 隐性记忆画像生成所需的最低 MemorySummary 节点数量
|
||||
MIN_MEMORY_SUMMARY_COUNT = 5
|
||||
|
||||
|
||||
class MemoryBaseService:
|
||||
"""记忆服务基类,提供共享的辅助方法"""
|
||||
|
||||
def __init__(self):
|
||||
self.neo4j_connector = Neo4jConnector()
|
||||
|
||||
async def get_valid_memory_summary_count(
|
||||
self,
|
||||
end_user_id: str
|
||||
) -> int:
|
||||
"""获取用户有效的 MemorySummary 节点数量(排除孤立节点)。
|
||||
|
||||
只统计存在 DERIVED_FROM_STATEMENT 关系的 MemorySummary 节点。
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
|
||||
Returns:
|
||||
有效 MemorySummary 节点数量
|
||||
"""
|
||||
try:
|
||||
query = """
|
||||
MATCH (n:MemorySummary)-[:DERIVED_FROM_STATEMENT]->(:Statement)
|
||||
WHERE n.end_user_id = $end_user_id
|
||||
RETURN count(DISTINCT n) as count
|
||||
"""
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query, end_user_id=end_user_id
|
||||
)
|
||||
count = result[0]["count"] if result and len(result) > 0 else 0
|
||||
logger.debug(
|
||||
f"有效 MemorySummary 节点数量: {count} (end_user_id={end_user_id})"
|
||||
)
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"获取有效 MemorySummary 数量失败: {str(e)}", exc_info=True
|
||||
)
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def parse_timestamp(timestamp_value) -> Optional[int]:
|
||||
"""
|
||||
|
||||
@@ -233,7 +233,7 @@ class MemoryPerceptualService:
|
||||
api_key=model_config.api_key,
|
||||
base_url=model_config.api_base,
|
||||
is_omni=model_config.is_omni,
|
||||
support_thinking="thinking" in (model_config.capability or []),
|
||||
capability=model_config.capability,
|
||||
)
|
||||
)
|
||||
return llm, model_config
|
||||
|
||||
@@ -47,7 +47,8 @@ class ModelParameterMerger:
|
||||
"n": 1,
|
||||
"stop": None,
|
||||
"deep_thinking": False,
|
||||
"thinking_budget_tokens": None
|
||||
"thinking_budget_tokens": None,
|
||||
"json_output": False
|
||||
}
|
||||
|
||||
# 合并参数:默认值 -> 模型配置 -> Agent 配置
|
||||
|
||||
@@ -125,9 +125,11 @@ class ModelConfigService:
|
||||
api_key=api_key,
|
||||
base_url=api_base,
|
||||
is_omni=is_omni,
|
||||
support_thinking="thinking" in (capability or []),
|
||||
temperature=0.7,
|
||||
max_tokens=100
|
||||
capability=capability,
|
||||
extra_params={
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 100
|
||||
}
|
||||
)
|
||||
|
||||
# 根据模型类型选择不同的验证方式
|
||||
@@ -729,10 +731,21 @@ class ModelApiKeyService:
|
||||
@staticmethod
|
||||
def delete_api_key(db: Session, api_key_id: uuid.UUID) -> bool:
|
||||
"""删除API Key"""
|
||||
if not ModelApiKeyRepository.get_by_id(db, api_key_id):
|
||||
api_key = ModelApiKeyRepository.get_by_id(db, api_key_id)
|
||||
if not api_key:
|
||||
raise BusinessException("API Key不存在", BizCode.NOT_FOUND)
|
||||
|
||||
model_config_ids = [mc.id for mc in api_key.model_configs]
|
||||
|
||||
success = ModelApiKeyRepository.delete(db, api_key_id)
|
||||
|
||||
for model_config_id in model_config_ids:
|
||||
model_config = ModelConfigRepository.get_by_id(db, model_config_id)
|
||||
if model_config:
|
||||
has_active_key = any(key.is_active for key in model_config.api_keys)
|
||||
if not has_active_key and model_config.is_active:
|
||||
model_config.is_active = False
|
||||
|
||||
db.commit()
|
||||
return success
|
||||
|
||||
|
||||
@@ -2616,9 +2616,11 @@ class MultiAgentOrchestrator:
|
||||
api_key=api_key_config.api_key,
|
||||
base_url=api_key_config.api_base,
|
||||
is_omni=api_key_config.is_omni,
|
||||
support_thinking="thinking" in (api_key_config.capability or []),
|
||||
temperature=0.7, # 整合任务使用中等温度
|
||||
max_tokens=2000
|
||||
capability=api_key_config.capability,
|
||||
extra_params={
|
||||
"temperature": 0.7, # 整合任务使用中等温度
|
||||
"max_tokens": 2000
|
||||
}
|
||||
)
|
||||
|
||||
# 创建 LLM 实例
|
||||
@@ -2795,10 +2797,12 @@ class MultiAgentOrchestrator:
|
||||
api_key=api_key_config.api_key,
|
||||
base_url=api_key_config.api_base,
|
||||
is_omni=api_key_config.is_omni,
|
||||
support_thinking="thinking" in (api_key_config.capability or []),
|
||||
temperature=0.7,
|
||||
max_tokens=2000,
|
||||
extra_params={"streaming": True} # 启用流式输出
|
||||
capability=api_key_config.capability,
|
||||
extra_params={
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2000,
|
||||
"streaming": True # 启用流式输出
|
||||
}
|
||||
)
|
||||
|
||||
# 创建 LLM 实例
|
||||
|
||||
@@ -186,7 +186,7 @@ class PromptOptimizerService:
|
||||
api_key=api_config.api_key,
|
||||
base_url=api_config.api_base,
|
||||
is_omni=api_config.is_omni,
|
||||
support_thinking="thinking" in (api_config.capability or []),
|
||||
capability=api_config.capability,
|
||||
), type=ModelType(model_config.type))
|
||||
try:
|
||||
prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompt')
|
||||
@@ -227,10 +227,20 @@ class PromptOptimizerService:
|
||||
content = getattr(chunk, "content", chunk)
|
||||
if not content:
|
||||
continue
|
||||
buffer += content
|
||||
if isinstance(content, str):
|
||||
buffer += content
|
||||
elif isinstance(content, list):
|
||||
for _ in content:
|
||||
buffer += _["text"]
|
||||
else:
|
||||
logger.error(f"Unsupported content type - {content}")
|
||||
raise Exception("Unsupported content type")
|
||||
cache = buffer[:-20]
|
||||
last_idx = 19
|
||||
while cache and cache[-1] == '\\' and last_idx > 0:
|
||||
cache = buffer[:-last_idx]
|
||||
last_idx -= 1
|
||||
|
||||
# 尝试找到 "prompt": " 开始位置
|
||||
if prompt_finished:
|
||||
continue
|
||||
|
||||
@@ -272,7 +282,7 @@ class PromptOptimizerService:
|
||||
def parser_prompt_variables(prompt: str):
|
||||
try:
|
||||
pattern = r'\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
|
||||
matches = re.findall(pattern, prompt)
|
||||
matches = re.findall(pattern, str(prompt))
|
||||
variables = list(set(matches))
|
||||
return variables
|
||||
except Exception as e:
|
||||
|
||||
@@ -250,7 +250,8 @@ class SharedChatService:
|
||||
tools=tools,
|
||||
deep_thinking=model_parameters.get("deep_thinking", False),
|
||||
thinking_budget_tokens=model_parameters.get("thinking_budget_tokens"),
|
||||
capability=api_key_obj.capability or [],
|
||||
json_output=model_parameters.get("json_output", False),
|
||||
capability=api_key_obj.capability,
|
||||
)
|
||||
|
||||
# 加载历史消息
|
||||
@@ -455,6 +456,7 @@ class SharedChatService:
|
||||
streaming=True,
|
||||
deep_thinking=model_parameters.get("deep_thinking", False),
|
||||
thinking_budget_tokens=model_parameters.get("thinking_budget_tokens"),
|
||||
json_output=model_parameters.get("json_output", False),
|
||||
capability=api_key_obj.capability or [],
|
||||
)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging_config import get_logger
|
||||
from app.core.memory.storage_services.extraction_engine.deduplication.deduped_and_disamb import _USER_PLACEHOLDER_NAMES
|
||||
from app.core.memory.utils.llm.llm_utils import MemoryClientFactory
|
||||
from app.db import get_db_context
|
||||
from app.repositories.conversation_repository import ConversationRepository
|
||||
@@ -21,7 +22,7 @@ from app.repositories.end_user_repository import EndUserRepository
|
||||
from app.repositories.neo4j.cypher_queries import Graph_Node_query
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from app.schemas.memory_episodic_schema import EmotionSubject, EmotionType, type_mapping
|
||||
from app.services.memory_base_service import MemoryBaseService
|
||||
from app.services.memory_base_service import MemoryBaseService, MIN_MEMORY_SUMMARY_COUNT
|
||||
from app.services.memory_config_service import MemoryConfigService
|
||||
from app.services.memory_perceptual_service import MemoryPerceptualService
|
||||
from app.services.memory_short_service import ShortService
|
||||
@@ -400,12 +401,21 @@ class UserMemoryService:
|
||||
# 构建响应数据(转换时间为毫秒时间戳)
|
||||
# 将 meta_data 中的 profile、knowledge_tags、behavioral_hints 平铺到顶层
|
||||
meta = end_user_info_record.meta_data or {}
|
||||
|
||||
# profile 列表字段截断:只返回前 MAX_PROFILE_LIST_SIZE 条(按时间从新到旧)
|
||||
MAX_PROFILE_LIST_SIZE = 5
|
||||
profile = meta.get("profile")
|
||||
if isinstance(profile, dict):
|
||||
for key in ("role", "domain", "expertise", "interests"):
|
||||
if isinstance(profile.get(key), list):
|
||||
profile[key] = profile[key][:MAX_PROFILE_LIST_SIZE]
|
||||
|
||||
response_data = {
|
||||
"end_user_info_id": str(end_user_info_record.id),
|
||||
"end_user_id": str(end_user_info_record.end_user_id),
|
||||
"other_name": end_user_info_record.other_name,
|
||||
"aliases": end_user_info_record.aliases,
|
||||
"profile": meta.get("profile"),
|
||||
"profile": profile,
|
||||
"knowledge_tags": meta.get("knowledge_tags"),
|
||||
"behavioral_hints": meta.get("behavioral_hints"),
|
||||
"created_at": datetime_to_timestamp(end_user_info_record.created_at),
|
||||
@@ -477,7 +487,7 @@ class UserMemoryService:
|
||||
allowed_fields = {'other_name', 'aliases', 'meta_data'}
|
||||
|
||||
# 用户占位名称黑名单,不允许作为 other_name 或出现在 aliases 中
|
||||
_user_placeholder_names = {'用户', '我', 'User', 'I'}
|
||||
_user_placeholder_names = _USER_PLACEHOLDER_NAMES
|
||||
|
||||
# 过滤 other_name:不允许设置为占位名称
|
||||
if 'other_name' in update_data and update_data['other_name'] and update_data['other_name'].strip() in _user_placeholder_names:
|
||||
@@ -1504,7 +1514,7 @@ async def analytics_memory_types(
|
||||
2. 工作记忆 (WORKING_MEMORY) = 会话数量(通过 ConversationRepository.get_conversation_by_user_id 获取)
|
||||
3. 短期记忆 (SHORT_TERM_MEMORY) = /short_term 接口返回的问答对数量
|
||||
4. 显性记忆 (EXPLICIT_MEMORY) = 情景记忆 + 语义记忆(通过 MemoryBaseService.get_explicit_memory_count 获取)
|
||||
5. 隐性记忆 (IMPLICIT_MEMORY) = Statement 节点数量的三分之一
|
||||
5. 隐性记忆 (IMPLICIT_MEMORY) = MemorySummary 节点数量(需 >= MIN_MEMORY_SUMMARY_COUNT 才显示,否则为 0)
|
||||
6. 情绪记忆 (EMOTIONAL_MEMORY) = 情绪标签统计总数(通过 MemoryBaseService.get_emotional_memory_count 获取)
|
||||
7. 情景记忆 (EPISODIC_MEMORY) = memory_summary(通过 MemoryBaseService.get_episodic_memory_count 获取)
|
||||
8. 遗忘记忆 (FORGET_MEMORY) = 激活值低于阈值的节点数(通过 MemoryBaseService.get_forget_memory_count 获取)
|
||||
@@ -1561,23 +1571,15 @@ async def analytics_memory_types(
|
||||
logger.warning(f"获取会话数量失败,工作记忆数量设为0: {str(e)}")
|
||||
work_count = 0
|
||||
|
||||
# 获取隐性记忆数量(基于 Statement 节点数量的三分之一)
|
||||
# 获取隐性记忆数量(基于有关联关系的 MemorySummary 节点数量,需 >= MIN_MEMORY_SUMMARY_COUNT 才计入)
|
||||
implicit_count = 0
|
||||
if end_user_id:
|
||||
try:
|
||||
# 查询 Statement 节点数量
|
||||
query = """
|
||||
MATCH (n:Statement)
|
||||
WHERE n.end_user_id = $end_user_id
|
||||
RETURN count(n) as count
|
||||
"""
|
||||
result = await _neo4j_connector.execute_query(query, end_user_id=end_user_id)
|
||||
statement_count = result[0]["count"] if result and len(result) > 0 else 0
|
||||
# 取三分之一作为隐性记忆数量
|
||||
implicit_count = round(statement_count / 3)
|
||||
logger.debug(f"隐性记忆数量(Statement数量的1/3): {implicit_count} (Statement总数={statement_count}, end_user_id={end_user_id})")
|
||||
memory_summary_count = await base_service.get_valid_memory_summary_count(end_user_id)
|
||||
implicit_count = memory_summary_count if memory_summary_count >= MIN_MEMORY_SUMMARY_COUNT else 0
|
||||
logger.debug(f"隐性记忆数量(有效MemorySummary节点数): {implicit_count} (有效MemorySummary总数={memory_summary_count}, end_user_id={end_user_id})")
|
||||
except Exception as e:
|
||||
logger.warning(f"获取Statement数量失败,隐性记忆数量设为0: {str(e)}")
|
||||
logger.warning(f"获取MemorySummary数量失败,隐性记忆数量设为0: {str(e)}")
|
||||
implicit_count = 0
|
||||
|
||||
# 原有的基于行为习惯的统计方式(已注释)
|
||||
@@ -1643,7 +1645,7 @@ async def analytics_memory_types(
|
||||
"WORKING_MEMORY": work_count, # 工作记忆(基于会话数量)
|
||||
"SHORT_TERM_MEMORY": short_term_count, # 短期记忆(基于问答对数量)
|
||||
"EXPLICIT_MEMORY": explicit_count, # 显性记忆(情景记忆 + 语义记忆)
|
||||
"IMPLICIT_MEMORY": implicit_count, # 隐性记忆(Statement数量的1/3)
|
||||
"IMPLICIT_MEMORY": implicit_count, # 隐性记忆(MemorySummary节点数,需>=MIN_MEMORY_SUMMARY_COUNT)
|
||||
"EMOTIONAL_MEMORY": emotion_count, # 情绪记忆(使用情绪标签统计)
|
||||
"EPISODIC_MEMORY": episodic_count, # 情景记忆
|
||||
"FORGET_MEMORY": forget_count # 遗忘记忆(激活值低于阈值)
|
||||
|
||||
@@ -285,7 +285,7 @@ def activate_user(db: Session, user_id_to_activate: uuid.UUID, current_user: Use
|
||||
try:
|
||||
# 查找用户
|
||||
business_logger.debug(f"查找待激活用户: {user_id_to_activate}")
|
||||
db_user = user_repository.get_user_by_id(db, user_id=user_id_to_activate)
|
||||
db_user = user_repository.get_user_by_id_regardless_active(db, user_id=user_id_to_activate)
|
||||
if not db_user:
|
||||
business_logger.warning(f"用户不存在: {user_id_to_activate}")
|
||||
raise BusinessException("用户不存在", code=BizCode.USER_NOT_FOUND)
|
||||
|
||||
@@ -957,7 +957,10 @@ class WorkflowService:
|
||||
for file in message["content"]:
|
||||
human_meta["files"].append({
|
||||
"type": file.get("type"),
|
||||
"url": file.get("url")
|
||||
"url": file.get("url"),
|
||||
"file_type": file.get("origin_file_type"),
|
||||
"name": file.get("name"),
|
||||
"size": file.get("size")
|
||||
})
|
||||
if message["role"] == "assistant":
|
||||
assistant_message = message["content"]
|
||||
|
||||
Reference in New Issue
Block a user