Add workflow support and enhance runtime configuration in models and services
- Introduce RuntimeModelResource and RuntimeKnowledgeBase classes to manage workflow resources. - Update AssistantConfig to include workflow_model_resources and workflow_knowledge_bases for better integration. - Refactor validation and processing logic in routes and services to accommodate workflow types. - Implement dynamic variable support for workflow assistants and enhance graph normalization. - Add ToolExecutor for reusable tool execution across different assistant types. - Update various services to ensure compatibility with new workflow features and improve error handling.
This commit is contained in:
@@ -15,7 +15,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import AssistantOut, AssistantUpsert
|
||||
from services.auth import require_admin
|
||||
from services.masking import mask, resolve_incoming_key
|
||||
from services.node_specs import validate_graph
|
||||
from services.node_specs import graph_references, normalize_graph, validate_graph
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -31,9 +31,45 @@ def _validate_workflow(body: AssistantUpsert) -> None:
|
||||
"""workflow 类型:保存前校验图结构,不通过则 400。其他类型跳过。"""
|
||||
if body.type != "workflow":
|
||||
return
|
||||
errors = validate_graph(body.graph or {})
|
||||
body.graph = normalize_graph(body.graph or {})
|
||||
errors = validate_graph(body.graph)
|
||||
if errors:
|
||||
raise HTTPException(400, "工作流校验失败:" + ";".join(errors))
|
||||
refs = graph_references(body.graph)
|
||||
body.tool_ids = list(dict.fromkeys([*body.tool_ids, *sorted(refs["tools"])]))
|
||||
|
||||
|
||||
async def _validate_workflow_references(
|
||||
session: AsyncSession, body: AssistantUpsert
|
||||
) -> None:
|
||||
if body.type != "workflow" or not body.graph.get("nodes"):
|
||||
return
|
||||
graph = body.graph
|
||||
settings = graph.get("settings") or {}
|
||||
resource_expectations: dict[str, str] = {}
|
||||
for key, capability in (
|
||||
("defaultAsrResourceId", "ASR"),
|
||||
("defaultTtsResourceId", "TTS"),
|
||||
):
|
||||
if settings.get(key):
|
||||
resource_expectations[str(settings[key])] = capability
|
||||
knowledge_ids: set[str] = set()
|
||||
for node in graph.get("nodes") or []:
|
||||
data = node.get("data") or {}
|
||||
if data.get("asrResourceId"):
|
||||
resource_expectations[str(data["asrResourceId"])] = "ASR"
|
||||
if data.get("ttsResourceId"):
|
||||
resource_expectations[str(data["ttsResourceId"])] = "TTS"
|
||||
if data.get("knowledgeBaseId"):
|
||||
knowledge_ids.add(str(data["knowledgeBaseId"]))
|
||||
for resource_id, capability in resource_expectations.items():
|
||||
resource = await session.get(ModelResource, resource_id)
|
||||
if not resource or not resource.enabled or resource.capability != capability:
|
||||
raise HTTPException(400, f"Workflow 引用了无效的 {capability} 资源:{resource_id}")
|
||||
for knowledge_id in knowledge_ids:
|
||||
knowledge = await session.get(KnowledgeBase, knowledge_id)
|
||||
if not knowledge or knowledge.status != "active":
|
||||
raise HTTPException(400, f"Workflow 引用了无效知识库:{knowledge_id}")
|
||||
|
||||
|
||||
async def _validate_vision_model(
|
||||
@@ -101,7 +137,11 @@ async def _resource_ids(session: AsyncSession, assistant_id: str) -> dict[str, s
|
||||
async def _sync_tool_bindings(
|
||||
session: AsyncSession, assistant_id: str, assistant_type: str, tool_ids: list[str]
|
||||
) -> None:
|
||||
requested = list(dict.fromkeys(tool_ids)) if assistant_type == "prompt" else []
|
||||
requested = (
|
||||
list(dict.fromkeys(tool_ids))
|
||||
if assistant_type in {"prompt", "workflow"}
|
||||
else []
|
||||
)
|
||||
if requested:
|
||||
tools = (
|
||||
await session.execute(select(Tool).where(Tool.id.in_(requested)))
|
||||
@@ -158,7 +198,11 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
||||
api_url=assistant.api_url,
|
||||
api_key=mask(assistant.api_key),
|
||||
app_id=assistant.app_id,
|
||||
graph=assistant.graph or {},
|
||||
graph=(
|
||||
normalize_graph(assistant.graph or {})
|
||||
if assistant.type == "workflow"
|
||||
else {}
|
||||
),
|
||||
updated_at=assistant.updated_at.isoformat() if assistant.updated_at else None,
|
||||
)
|
||||
|
||||
@@ -176,6 +220,7 @@ async def create_assistant(
|
||||
body: AssistantUpsert, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
_validate_workflow(body)
|
||||
await _validate_workflow_references(session, body)
|
||||
await _validate_vision_model(session, body)
|
||||
await _validate_knowledge_base(session, body)
|
||||
data = body.model_dump()
|
||||
@@ -248,6 +293,7 @@ async def update_assistant(
|
||||
if not assistant:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
_validate_workflow(body)
|
||||
await _validate_workflow_references(session, body)
|
||||
await _validate_vision_model(session, body)
|
||||
await _validate_knowledge_base(session, body)
|
||||
data = body.model_dump()
|
||||
|
||||
@@ -15,6 +15,7 @@ from schemas import (
|
||||
)
|
||||
from services.auth import require_admin
|
||||
from services.knowledge import create_document, delete_storage_object, process_document, search
|
||||
from services.node_specs import graph_references
|
||||
import settings
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@@ -123,6 +124,14 @@ async def delete_knowledge_base(
|
||||
)).scalar_one_or_none()
|
||||
if referenced:
|
||||
raise HTTPException(409, "知识库正被助手引用,无法删除")
|
||||
workflows = (
|
||||
await session.execute(select(Assistant).where(Assistant.type == "workflow"))
|
||||
).scalars().all()
|
||||
if any(
|
||||
kb_id in graph_references(assistant.graph or {})["knowledge_bases"]
|
||||
for assistant in workflows
|
||||
):
|
||||
raise HTTPException(409, "知识库正被 Workflow 节点引用,无法删除")
|
||||
documents = (await session.execute(
|
||||
select(KnowledgeDocument).where(KnowledgeDocument.knowledge_base_id == kb_id)
|
||||
)).scalars().all()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import uuid
|
||||
|
||||
from db.models import (
|
||||
Assistant,
|
||||
AssistantModelBinding,
|
||||
InterfaceDefinition,
|
||||
KnowledgeBase,
|
||||
@@ -20,6 +21,7 @@ from services.auth import require_admin
|
||||
from services.interface_catalog import validate_fields
|
||||
from services.masking import mask_secrets, merge_secrets
|
||||
from services.model_resource_tester import test_model_resource
|
||||
from services.node_specs import graph_references
|
||||
from sqlalchemy import delete, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -102,6 +104,14 @@ async def _clear_incompatible_references(
|
||||
) -> None:
|
||||
if capability == resource.capability:
|
||||
return
|
||||
workflows = (
|
||||
await session.execute(select(Assistant).where(Assistant.type == "workflow"))
|
||||
).scalars().all()
|
||||
if any(
|
||||
resource.id in graph_references(assistant.graph or {})["model_resources"]
|
||||
for assistant in workflows
|
||||
):
|
||||
raise HTTPException(409, "模型资源正被 Workflow 节点引用,不能修改能力类型")
|
||||
await session.execute(
|
||||
delete(AssistantModelBinding).where(
|
||||
AssistantModelBinding.model_resource_id == resource.id
|
||||
@@ -263,6 +273,14 @@ async def delete_model_resource(
|
||||
).scalar_one_or_none()
|
||||
if in_use:
|
||||
raise HTTPException(409, "该模型资源仍被助手引用")
|
||||
workflows = (
|
||||
await session.execute(select(Assistant).where(Assistant.type == "workflow"))
|
||||
).scalars().all()
|
||||
if any(
|
||||
resource_id in graph_references(assistant.graph or {})["model_resources"]
|
||||
for assistant in workflows
|
||||
):
|
||||
raise HTTPException(409, "该模型资源仍被 Workflow 节点引用")
|
||||
await session.delete(row)
|
||||
await session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
Reference in New Issue
Block a user