446 lines
20 KiB
Python
446 lines
20 KiB
Python
"""应用 DSL 导入导出服务"""
|
||
import uuid
|
||
import datetime
|
||
from typing import Optional
|
||
|
||
import yaml
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.config import settings
|
||
from app.core.error_codes import BizCode
|
||
from app.core.exceptions import BusinessException, ResourceNotFoundException
|
||
from app.models import AgentConfig, MultiAgentConfig
|
||
from app.models.app_model import App, AppType
|
||
from app.models.app_release_model import AppRelease
|
||
from app.models.knowledge_model import Knowledge
|
||
from app.models.models_model import ModelConfig
|
||
from app.models.tool_model import ToolConfig as ToolConfigModel
|
||
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.models.memory_config_model import MemoryConfig as MemoryConfigModel
|
||
|
||
|
||
class AppDslService:
|
||
|
||
def __init__(self, db: Session):
|
||
self.db = db
|
||
|
||
# ==================== 导出 ====================
|
||
|
||
def export_dsl(self, app_id: uuid.UUID, release_id: Optional[uuid.UUID] = None) -> tuple[str, str]:
|
||
"""构建应用 DSL yaml 字符串,返回 (yaml_str, filename)"""
|
||
app = self.db.query(App).filter(App.id == app_id, App.is_active.is_(True)).first()
|
||
if not app:
|
||
raise ResourceNotFoundException("应用", str(app_id))
|
||
|
||
meta = {
|
||
"version": settings.SYSTEM_VERSION,
|
||
"platform": "MemoryBear",
|
||
"exported_at": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
||
}
|
||
app_meta = {
|
||
"name": app.name,
|
||
"description": app.description,
|
||
"icon": app.icon,
|
||
"icon_type": app.icon_type,
|
||
"type": app.type,
|
||
"tags": app.tags or [],
|
||
}
|
||
|
||
if release_id is not None:
|
||
return self._export_release(app, release_id, meta, app_meta)
|
||
|
||
return self._export_draft(app, meta, app_meta)
|
||
|
||
def _export_release(self, app: App, release_id: uuid.UUID, meta: dict, app_meta: dict) -> tuple[str, str]:
|
||
release = self.db.query(AppRelease).filter(
|
||
AppRelease.app_id == app.id,
|
||
AppRelease.id == release_id,
|
||
AppRelease.is_active.is_(True)
|
||
).first()
|
||
if not release:
|
||
raise ResourceNotFoundException("版本", str(release_id))
|
||
|
||
meta["release_version"] = release.version
|
||
meta["release_name"] = release.version_name
|
||
app_meta["name"] = release.name
|
||
app_meta["description"] = release.description
|
||
config_key = {
|
||
AppType.AGENT: "agent_config",
|
||
AppType.MULTI_AGENT: "multi_agent_config",
|
||
AppType.WORKFLOW: "workflow"
|
||
}.get(app.type, "config")
|
||
config_data = self._enrich_release_config(app.type, release.config or {})
|
||
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:
|
||
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"])
|
||
if "knowledge_retrieval" in cfg:
|
||
enriched["knowledge_retrieval"] = self._enrich_knowledge_retrieval(cfg["knowledge_retrieval"])
|
||
if "tools" in cfg:
|
||
enriched["tools"] = self._enrich_tools(cfg["tools"])
|
||
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"])
|
||
if "master_agent_id" in cfg:
|
||
enriched["master_agent_ref"] = self._release_ref(cfg["master_agent_id"])
|
||
if "sub_agents" in cfg:
|
||
enriched["sub_agents"] = self._enrich_sub_agents(cfg["sub_agents"])
|
||
if "routing_rules" in cfg:
|
||
enriched["routing_rules"] = [
|
||
{**r, "_ref": self._agent_ref(r.get("target_agent_id"))} for r in (cfg["routing_rules"] or [])
|
||
]
|
||
return enriched
|
||
return cfg
|
||
|
||
def _export_draft(self, app: App, meta: dict, app_meta: dict) -> tuple[str, str]:
|
||
if app.type == AppType.WORKFLOW:
|
||
config = self.db.query(WorkflowConfig).filter(WorkflowConfig.app_id == app.id).first()
|
||
config_data = {
|
||
"variables": config.variables if config else [],
|
||
"edges": config.edges if config else [],
|
||
"nodes": config.nodes if config else [],
|
||
"execution_config": config.execution_config if config else {},
|
||
"triggers": config.triggers if config else [],
|
||
} if config else {}
|
||
dsl = {**meta, "app": app_meta, "workflow": config_data}
|
||
|
||
elif app.type == AppType.AGENT:
|
||
config = self.db.query(AgentConfig).filter(AgentConfig.app_id == app.id).first()
|
||
config_data = {
|
||
"system_prompt": config.system_prompt if config else None,
|
||
"model_parameters": self._to_dict(config.model_parameters) if config else None,
|
||
"default_model_config_ref": self._model_ref(config.default_model_config_id) if config else None,
|
||
"knowledge_retrieval": self._enrich_knowledge_retrieval(config.knowledge_retrieval) if config else None,
|
||
"memory": config.memory if config else None,
|
||
"variables": config.variables if config else [],
|
||
"tools": self._enrich_tools(config.tools) if config else [],
|
||
"skills": config.skills if config else {},
|
||
} if config else {}
|
||
dsl = {**meta, "app": app_meta, "agent_config": config_data}
|
||
|
||
elif app.type == AppType.MULTI_AGENT:
|
||
config = self.db.query(MultiAgentConfig).filter(MultiAgentConfig.app_id == app.id).first()
|
||
config_data = {
|
||
"orchestration_mode": config.orchestration_mode if config else None,
|
||
"master_agent_name": config.master_agent_name if config else None,
|
||
"model_parameters": self._to_dict(config.model_parameters) if config else None,
|
||
"default_model_config_ref": self._model_ref(config.default_model_config_id) if config else None,
|
||
"master_agent_ref": self._release_ref(config.master_agent_id) if config else None,
|
||
"sub_agents": self._enrich_sub_agents(config.sub_agents) if config else [],
|
||
"routing_rules": [
|
||
{**r, "_ref": self._agent_ref(r.get("target_agent_id"))} for r in (config.routing_rules or [])
|
||
] if config else [],
|
||
|
||
"execution_config": config.execution_config if config else {},
|
||
"aggregation_strategy": config.aggregation_strategy if config else "merge",
|
||
} if config else {}
|
||
dsl = {**meta, "app": app_meta, "multi_agent_config": config_data}
|
||
|
||
else:
|
||
raise BusinessException(f"不支持的应用类型: {app.type}", BizCode.BAD_REQUEST)
|
||
|
||
return yaml.dump(dsl, default_flow_style=False, allow_unicode=True), f"{app.name}.yaml"
|
||
|
||
def _to_dict(self, value):
|
||
"""将 Pydantic 对象转为普通 dict,供 yaml.dump 安全序列化"""
|
||
if value is None:
|
||
return None
|
||
if hasattr(value, "model_dump"):
|
||
return value.model_dump()
|
||
return value
|
||
|
||
def _model_ref(self, model_config_id) -> Optional[dict]:
|
||
if not model_config_id:
|
||
return None
|
||
m = self.db.query(ModelConfig).filter(ModelConfig.id == model_config_id).first()
|
||
return {"id": str(model_config_id), "name": m.name, "provider": m.provider, "type": m.type} if m else {"id": str(model_config_id)}
|
||
|
||
def _kb_ref(self, kb_id) -> Optional[dict]:
|
||
if not kb_id:
|
||
return None
|
||
kb = self.db.query(Knowledge).filter(Knowledge.id == kb_id).first()
|
||
return {"id": str(kb_id), "name": kb.name} if kb else {"id": str(kb_id)}
|
||
|
||
def _tool_ref(self, tool_id) -> Optional[dict]:
|
||
if not tool_id:
|
||
return None
|
||
t = self.db.query(ToolConfigModel).filter(ToolConfigModel.id == tool_id).first()
|
||
return {"id": str(tool_id), "name": t.name, "tool_type": t.tool_type} if t else {"id": str(tool_id)}
|
||
|
||
def _enrich_knowledge_retrieval(self, kr: Optional[dict]) -> Optional[dict]:
|
||
if not kr:
|
||
return kr
|
||
kbs = [{**kb, "_ref": self._kb_ref(kb.get("kb_id"))} for kb in kr.get("knowledge_bases", [])]
|
||
return {**kr, "knowledge_bases": kbs}
|
||
|
||
def _enrich_tools(self, tools: list) -> list:
|
||
return [{**t, "_ref": self._tool_ref(t.get("tool_id"))} for t in (tools or [])]
|
||
|
||
def _agent_ref(self, agent_id) -> Optional[dict]:
|
||
if not agent_id:
|
||
return None
|
||
a = self.db.query(App).filter(App.id == agent_id).first()
|
||
return {"id": str(agent_id), "name": a.name} if a else {"id": str(agent_id)}
|
||
|
||
def _release_ref(self, release_id) -> Optional[dict]:
|
||
if not release_id:
|
||
return None
|
||
r = self.db.query(AppRelease).filter(AppRelease.id == release_id).first()
|
||
return {"id": str(release_id), "name": r.name, "version": r.version, "app_id": str(r.app_id)} if r else {"id": str(release_id)}
|
||
|
||
def _enrich_sub_agents(self, sub_agents: list) -> list:
|
||
return [{**s, "_ref": self._agent_ref(s.get("agent_id"))} for s in (sub_agents or [])]
|
||
|
||
# ==================== 导入 ====================
|
||
|
||
def import_dsl(
|
||
self,
|
||
dsl: dict,
|
||
workspace_id: uuid.UUID,
|
||
tenant_id: uuid.UUID,
|
||
user_id: uuid.UUID,
|
||
) -> tuple[App, list[str]]:
|
||
"""解析 DSL,创建应用及配置,返回 (new_app, warnings)"""
|
||
app_meta = dsl.get("app", {})
|
||
app_type = app_meta.get("type")
|
||
if app_type not in (AppType.AGENT, AppType.MULTI_AGENT, AppType.WORKFLOW):
|
||
raise BusinessException(f"不支持的应用类型: {app_type}", BizCode.BAD_REQUEST)
|
||
|
||
warnings: list[str] = []
|
||
now = datetime.datetime.now()
|
||
|
||
new_app = App(
|
||
id=uuid.uuid4(),
|
||
workspace_id=workspace_id,
|
||
created_by=user_id,
|
||
name=self._unique_app_name(app_meta.get("name", "导入应用"), workspace_id, app_type),
|
||
description=app_meta.get("description"),
|
||
icon=app_meta.get("icon"),
|
||
icon_type=app_meta.get("icon_type"),
|
||
type=app_type,
|
||
visibility="private",
|
||
status="draft",
|
||
tags=app_meta.get("tags", []),
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
self.db.add(new_app)
|
||
self.db.flush()
|
||
|
||
if app_type == AppType.AGENT:
|
||
cfg = dsl.get("agent_config") or {}
|
||
self.db.add(AgentConfig(
|
||
id=uuid.uuid4(),
|
||
app_id=new_app.id,
|
||
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),
|
||
knowledge_retrieval=self._resolve_knowledge_retrieval(cfg.get("knowledge_retrieval"), workspace_id, warnings),
|
||
memory=self._resolve_memory(cfg.get("memory"), workspace_id, warnings),
|
||
variables=cfg.get("variables", []),
|
||
tools=self._resolve_tools(cfg.get("tools", []), tenant_id, warnings),
|
||
skills=cfg.get("skills", {}),
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now,
|
||
))
|
||
|
||
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,
|
||
orchestration_mode=cfg.get("orchestration_mode", "collaboration"),
|
||
master_agent_name=cfg.get("master_agent_name"),
|
||
model_parameters=cfg.get("model_parameters"),
|
||
default_model_config_id=self._resolve_model(cfg.get("default_model_config_ref"), tenant_id, warnings),
|
||
master_agent_id=self._resolve_release(cfg.get("master_agent_ref"), warnings),
|
||
sub_agents=self._resolve_sub_agents(cfg.get("sub_agents", []), warnings),
|
||
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,
|
||
))
|
||
|
||
elif app_type == AppType.WORKFLOW:
|
||
adapter = MemoryBearAdapter(dsl)
|
||
if not adapter.validate_config():
|
||
raise BusinessException("工作流配置格式无效", BizCode.BAD_REQUEST)
|
||
result = adapter.parse_workflow()
|
||
for e in result.errors:
|
||
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", {}),
|
||
triggers=wf.get("triggers", []),
|
||
validate=False,
|
||
)
|
||
|
||
self.db.commit()
|
||
self.db.refresh(new_app)
|
||
return new_app, warnings
|
||
|
||
def _unique_app_name(self, name: str, workspace_id: uuid.UUID, app_type: AppType) -> str:
|
||
existing = {r[0] for r in self.db.query(App.name).filter(
|
||
App.workspace_id == workspace_id,
|
||
App.type == app_type,
|
||
App.is_active.is_(True)
|
||
).all()}
|
||
if name not in existing:
|
||
return name
|
||
counter = 1
|
||
while f"{name}({counter})" in existing:
|
||
counter += 1
|
||
return f"{name}({counter})"
|
||
|
||
def _resolve_model(self, ref: Optional[dict], tenant_id: uuid.UUID, warnings: list) -> Optional[uuid.UUID]:
|
||
if not ref:
|
||
return None
|
||
q = self.db.query(ModelConfig).filter(
|
||
ModelConfig.tenant_id == tenant_id,
|
||
ModelConfig.name == ref.get("name"),
|
||
ModelConfig.is_active.is_(True)
|
||
)
|
||
if ref.get("provider"):
|
||
q = q.filter(ModelConfig.provider == ref["provider"])
|
||
if ref.get("type"):
|
||
q = q.filter(ModelConfig.type == ref["type"])
|
||
m = q.first()
|
||
if not m:
|
||
warnings.append(f"模型 '{ref.get('name')}' 未匹配,已置空,请导入后手动配置")
|
||
return m.id if m else None
|
||
|
||
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
|
||
|
||
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
|
||
|
||
def _resolve_release(self, ref: Optional[dict], warnings: list) -> Optional[uuid.UUID]:
|
||
if not ref:
|
||
return None
|
||
r = self.db.query(AppRelease).filter(
|
||
AppRelease.app_id == ref.get("app_id"),
|
||
AppRelease.version == ref.get("version"),
|
||
AppRelease.is_active.is_(True)
|
||
).first()
|
||
if not r:
|
||
warnings.append(f"主 Agent 发布版本 '{ref.get('name')}' 未匹配,已置空,请导入后手动配置")
|
||
return r.id if r else None
|
||
|
||
def _resolve_sub_agents(self, sub_agents: list, warnings: list) -> list:
|
||
result = []
|
||
for s in (sub_agents or []):
|
||
ref = s.get("_ref")
|
||
entry = {k: v for k, v in s.items() if k != "_ref"}
|
||
if ref:
|
||
a = self.db.query(App).filter(App.name == ref.get("name"), App.is_active.is_(True)).first()
|
||
if not a:
|
||
warnings.append(f"子 Agent '{ref.get('name')}' 未匹配,已置空,请导入后手动配置")
|
||
entry["agent_id"] = str(a.id) if a else None
|
||
result.append(entry)
|
||
return result
|
||
|
||
def _resolve_routing_rules(self, rules: Optional[list], warnings: list) -> Optional[list]:
|
||
if rules is None:
|
||
return None
|
||
result = []
|
||
for r in rules:
|
||
ref = r.get("_ref")
|
||
entry = {k: v for k, v in r.items() if k != "_ref"}
|
||
if ref:
|
||
a = self.db.query(App).filter(App.name == ref.get("name"), App.is_active.is_(True)).first()
|
||
if not a:
|
||
warnings.append(f"路由目标 Agent '{ref.get('name')}' 未匹配,已置空,请导入后手动配置")
|
||
entry["target_agent_id"] = str(a.id) if a else None
|
||
result.append(entry)
|
||
return result
|
||
|
||
def _resolve_knowledge_retrieval(self, kr: Optional[dict], workspace_id: uuid.UUID, warnings: list) -> Optional[dict]:
|
||
if not kr:
|
||
return kr
|
||
resolved_kbs = []
|
||
for kb in kr.get("knowledge_bases", []):
|
||
ref = kb.get("_ref") or ({"name": kb.get("kb_id")} if kb.get("kb_id") else None)
|
||
entry = {k: v for k, v in kb.items() if k != "_ref"}
|
||
resolved_id = self._resolve_kb(ref, workspace_id, warnings)
|
||
if resolved_id is None:
|
||
continue
|
||
entry["kb_id"] = resolved_id
|
||
resolved_kbs.append(entry)
|
||
return {k: v for k, v in kr.items() if k != "knowledge_bases"} | {"knowledge_bases": resolved_kbs}
|
||
|
||
def _resolve_memory(self, memory: Optional[dict], workspace_id: uuid.UUID, warnings: list) -> Optional[dict]:
|
||
if not memory:
|
||
return memory
|
||
config_id = memory.get("memory_config_id") or memory.get("memory_content")
|
||
if not config_id:
|
||
return memory
|
||
try:
|
||
config_uuid = uuid.UUID(str(config_id))
|
||
except (ValueError, AttributeError):
|
||
exists = self.db.query(MemoryConfigModel).filter(
|
||
MemoryConfigModel.config_id_old == int(config_id),
|
||
MemoryConfigModel.workspace_id == workspace_id
|
||
).first()
|
||
if not exists:
|
||
warnings.append(f"记忆配置 '{config_id}' 未匹配,已置空,请导入后手动配置")
|
||
return {**memory, "memory_config_id": None, "enabled": False}
|
||
return memory
|
||
exists = self.db.query(MemoryConfigModel).filter(
|
||
MemoryConfigModel.config_id == config_uuid,
|
||
MemoryConfigModel.workspace_id == workspace_id
|
||
).first()
|
||
if not exists:
|
||
warnings.append(f"记忆配置 '{config_id}' 未匹配,已置空,请导入后手动配置")
|
||
return {**memory, "memory_config_id": None, "enabled": False}
|
||
return memory
|
||
|
||
def _resolve_tools(self, tools: list, tenant_id: uuid.UUID, warnings: list) -> list:
|
||
result = []
|
||
for t in (tools or []):
|
||
ref = t.get("_ref") or ({"name": t.get("tool_id")} if t.get("tool_id") else None)
|
||
entry = {k: v for k, v in t.items() if k != "_ref"}
|
||
resolved_id = self._resolve_tool(ref, tenant_id, warnings)
|
||
if resolved_id is None:
|
||
continue
|
||
entry["tool_id"] = resolved_id
|
||
result.append(entry)
|
||
return result
|